cloudron 6.0.0 → 7.0.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/CHANGELOG.md +4 -0
- package/bin/cloudron +15 -29
- package/bin/cloudron-appstore +4 -7
- package/bin/cloudron-build +18 -8
- package/bin/cloudron-versions +33 -0
- package/eslint.config.js +7 -6
- package/package.json +16 -15
- package/src/actions.js +230 -128
- package/src/appstore-actions.js +31 -59
- package/src/backup-tools.js +22 -24
- package/src/build-actions.js +113 -99
- package/src/completion.js +4 -8
- package/src/config.js +78 -53
- package/src/helper.js +155 -13
- package/src/readline.js +8 -10
- package/src/templates/CloudronManifest.appstore.json.ejs +2 -2
- package/src/versions-actions.js +184 -0
- package/test/test.js +131 -160
- package/src/superagent.js +0 -225
package/src/actions.js
CHANGED
|
@@ -1,59 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
exports = module.exports = {
|
|
24
|
-
list,
|
|
25
|
-
login,
|
|
26
|
-
logout,
|
|
27
|
-
open,
|
|
28
|
-
install,
|
|
29
|
-
setLocation,
|
|
30
|
-
debug,
|
|
31
|
-
update,
|
|
32
|
-
uninstall,
|
|
33
|
-
logs,
|
|
34
|
-
exec,
|
|
35
|
-
status,
|
|
36
|
-
inspect,
|
|
37
|
-
pull,
|
|
38
|
-
push,
|
|
39
|
-
restart,
|
|
40
|
-
start,
|
|
41
|
-
stop,
|
|
42
|
-
repair,
|
|
43
|
-
init,
|
|
44
|
-
restore,
|
|
45
|
-
importApp,
|
|
46
|
-
exportApp,
|
|
47
|
-
clone,
|
|
48
|
-
backupCreate,
|
|
49
|
-
backupList,
|
|
50
|
-
cancel,
|
|
51
|
-
|
|
52
|
-
envGet,
|
|
53
|
-
envSet,
|
|
54
|
-
envList,
|
|
55
|
-
envUnset
|
|
56
|
-
};
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { dockerignoreMatcher } from './build-actions.js';
|
|
3
|
+
import * as config from './config.js';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { exit, locateManifest, performOidcLogin } from './helper.js';
|
|
6
|
+
import { EventSource } from 'eventsource';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import https from 'https';
|
|
9
|
+
import manifestFormat from '@cloudron/manifest-format';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import * as readline from './readline.js';
|
|
13
|
+
import safe from '@cloudron/safetydance';
|
|
14
|
+
import { spawn } from 'child_process';
|
|
15
|
+
import semver from 'semver';
|
|
16
|
+
import stream from 'stream/promises';
|
|
17
|
+
import superagent from '@cloudron/superagent';
|
|
18
|
+
import Table from 'easy-table';
|
|
19
|
+
import tar from 'tar-fs';
|
|
20
|
+
import timers from 'timers/promises';
|
|
21
|
+
import zlib from 'zlib';
|
|
22
|
+
import open from 'open';
|
|
57
23
|
|
|
58
24
|
const NO_APP_FOUND_ERROR_STRING = 'Could not determine app. Use --app to specify the app\n';
|
|
59
25
|
|
|
@@ -158,7 +124,7 @@ async function selectAppWithRepository(repository, options) {
|
|
|
158
124
|
console.log('[%s]\t%s', index, app.fqdn);
|
|
159
125
|
});
|
|
160
126
|
|
|
161
|
-
let index
|
|
127
|
+
let index;
|
|
162
128
|
while (true) {
|
|
163
129
|
index = parseInt(await readline.question('Choose app [0-' + (matchingApps.length-1) + ']: ', {}), 10);
|
|
164
130
|
if (isNaN(index) || index < 0 || index > matchingApps.length-1) console.log('Invalid selection');
|
|
@@ -180,7 +146,7 @@ async function getApp(options) {
|
|
|
180
146
|
if (!manifestFilePath) throw new Error(NO_APP_FOUND_ERROR_STRING);
|
|
181
147
|
|
|
182
148
|
const sourceDir = path.dirname(manifestFilePath);
|
|
183
|
-
const appConfig = config.
|
|
149
|
+
const appConfig = config.getAppBuildConfig(sourceDir);
|
|
184
150
|
|
|
185
151
|
if (!appConfig.repository) throw new Error(NO_APP_FOUND_ERROR_STRING);
|
|
186
152
|
|
|
@@ -300,7 +266,7 @@ async function stopApp(app, options) {
|
|
|
300
266
|
const response = await request.send({});
|
|
301
267
|
if (response.status !== 202) throw `Failed to stop app: ${requestError(response)}`;
|
|
302
268
|
|
|
303
|
-
|
|
269
|
+
return response.body.taskId;
|
|
304
270
|
}
|
|
305
271
|
|
|
306
272
|
async function startApp(app, options) {
|
|
@@ -311,7 +277,7 @@ async function startApp(app, options) {
|
|
|
311
277
|
const response = await request.send({});
|
|
312
278
|
if (response.status !== 202) throw `Failed to start app: ${requestError(response)}`;
|
|
313
279
|
|
|
314
|
-
|
|
280
|
+
return response.body.taskId;
|
|
315
281
|
}
|
|
316
282
|
|
|
317
283
|
async function restartApp(app, options) {
|
|
@@ -322,7 +288,7 @@ async function restartApp(app, options) {
|
|
|
322
288
|
const response = await request.send({});
|
|
323
289
|
if (response.status !== 202) throw `Failed to restart app: ${requestError(response)}`;
|
|
324
290
|
|
|
325
|
-
|
|
291
|
+
return response.body.taskId;
|
|
326
292
|
}
|
|
327
293
|
|
|
328
294
|
async function authenticate(adminFqdn, username, password, options) {
|
|
@@ -389,12 +355,24 @@ async function login(adminFqdn, localOptions, cmd) {
|
|
|
389
355
|
}
|
|
390
356
|
|
|
391
357
|
if (!token) {
|
|
392
|
-
|
|
393
|
-
const
|
|
358
|
+
// oidc cli login is possible from 9.1 on
|
|
359
|
+
const request = superagent.get(`https://${adminFqdn}/api/v1/cloudron/status`).ok(() => true);
|
|
360
|
+
if (!rejectUnauthorized) request.disableTLSCerts();
|
|
394
361
|
|
|
395
|
-
const [error,
|
|
396
|
-
if (error) return exit(
|
|
397
|
-
|
|
362
|
+
const [error, response] = await safe(request);
|
|
363
|
+
if (error) return exit(error);
|
|
364
|
+
if (response.status !== 200) return exit(`Failed to get cloudron status: ${requestError(response)}`);
|
|
365
|
+
|
|
366
|
+
if (semver.gte(response.body.version, '9.1.0')) {
|
|
367
|
+
token = await performOidcLogin(adminFqdn, { rejectUnauthorized });
|
|
368
|
+
} else {
|
|
369
|
+
const username = options.username || await readline.question('Username: ', {});
|
|
370
|
+
const password = options.password || await readline.question('Password: ', { noEchoBack: true });
|
|
371
|
+
|
|
372
|
+
const [authError, result] = await safe(authenticate(adminFqdn, username, password, { rejectUnauthorized, askForTotpToken: false }));
|
|
373
|
+
if (authError) return exit(`Failed to login: ${authError.message}`);
|
|
374
|
+
token = result;
|
|
375
|
+
}
|
|
398
376
|
}
|
|
399
377
|
|
|
400
378
|
config.setActive(adminFqdn);
|
|
@@ -404,6 +382,8 @@ async function login(adminFqdn, localOptions, cmd) {
|
|
|
404
382
|
config.set('cloudrons.default', adminFqdn);
|
|
405
383
|
|
|
406
384
|
console.log('Login successful.');
|
|
385
|
+
|
|
386
|
+
process.exit(0);
|
|
407
387
|
}
|
|
408
388
|
|
|
409
389
|
function logout() {
|
|
@@ -413,15 +393,14 @@ function logout() {
|
|
|
413
393
|
console.log('Logged out.');
|
|
414
394
|
}
|
|
415
395
|
|
|
416
|
-
async function
|
|
396
|
+
async function openApp(localOptions, cmd) {
|
|
417
397
|
const options = cmd.optsWithGlobals();
|
|
418
398
|
const [error, app] = await safe(getApp(options));
|
|
419
399
|
if (error) return exit(error);
|
|
420
400
|
|
|
421
401
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
422
402
|
|
|
423
|
-
|
|
424
|
-
(await import('open')).default(`https://${app.fqdn}`);
|
|
403
|
+
open(`https://${app.fqdn}`);
|
|
425
404
|
}
|
|
426
405
|
|
|
427
406
|
async function list(localOptions, cmd) {
|
|
@@ -448,12 +427,12 @@ async function list(localOptions, cmd) {
|
|
|
448
427
|
responses.forEach(function (result) {
|
|
449
428
|
if (result.status !== 'fulfilled') return exit(`Failed to list app: ${requestError(result.value)}`);
|
|
450
429
|
|
|
451
|
-
const
|
|
430
|
+
const appResponse = result.value;
|
|
452
431
|
|
|
453
|
-
if (
|
|
454
|
-
|
|
432
|
+
if (appResponse.status !== 200) return exit(`Failed to list app: ${requestError(appResponse)}`);
|
|
433
|
+
appResponse.body.location = appResponse.body.location || appResponse.body.subdomain; // LEGACY support
|
|
455
434
|
|
|
456
|
-
const detailedApp =
|
|
435
|
+
const detailedApp = appResponse.body;
|
|
457
436
|
|
|
458
437
|
t.cell('Id', detailedApp.id);
|
|
459
438
|
t.cell('Location', detailedApp.fqdn);
|
|
@@ -578,13 +557,32 @@ async function install(localOptions, cmd) {
|
|
|
578
557
|
const result = await getManifest(options.appstoreId || '');
|
|
579
558
|
const { manifest, manifestFilePath } = result;
|
|
580
559
|
|
|
560
|
+
let sourceArchiveFilePath = null; // will be set if we need to send a tarball
|
|
561
|
+
|
|
581
562
|
if (!manifest.dockerImage) { // not a manifest from appstore
|
|
582
563
|
const sourceDir = path.dirname(manifestFilePath);
|
|
583
|
-
const image = options.image || config.
|
|
564
|
+
const image = options.image || config.getAppBuildConfig(sourceDir).dockerImage;
|
|
584
565
|
|
|
585
|
-
if (!image)
|
|
566
|
+
if (!image) {
|
|
567
|
+
console.log('No build detected. This package will be built on the server.');
|
|
586
568
|
|
|
587
|
-
|
|
569
|
+
const dockerignoreFilePath = path.join(sourceDir, '.dockerignore');
|
|
570
|
+
const ignoreMatcher = dockerignoreMatcher(dockerignoreFilePath);
|
|
571
|
+
|
|
572
|
+
const tarStream = tar.pack(sourceDir, {
|
|
573
|
+
ignore: function (name) {
|
|
574
|
+
return ignoreMatcher(name.slice(sourceDir.length + 1)); // make name as relative path
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
sourceArchiveFilePath = path.join(os.tmpdir(), `cloudron-source-${Date.now()}.tar`);
|
|
579
|
+
const sourceArchiveStream = fs.createWriteStream(sourceArchiveFilePath);
|
|
580
|
+
|
|
581
|
+
const [tarError] = await safe(stream.pipeline(tarStream, sourceArchiveStream));
|
|
582
|
+
if (tarError) return exit(`Could not create source archive: ${tarError.message}`);
|
|
583
|
+
} else {
|
|
584
|
+
manifest.dockerImage = image;
|
|
585
|
+
}
|
|
588
586
|
}
|
|
589
587
|
|
|
590
588
|
const location = options.location || await readline.question('Location: ', {});
|
|
@@ -689,14 +687,43 @@ async function install(localOptions, cmd) {
|
|
|
689
687
|
if (!data.icon) return exit(`Could not read icon: ${iconFilename} message: ${safe.error.message}`);
|
|
690
688
|
}
|
|
691
689
|
|
|
692
|
-
const request = createRequest('POST', '/api/v1/apps
|
|
693
|
-
|
|
690
|
+
const request = createRequest('POST', '/api/v1/apps', options);
|
|
691
|
+
|
|
692
|
+
let response;
|
|
693
|
+
if (sourceArchiveFilePath) {
|
|
694
|
+
// Send as multipart form-data with source archive
|
|
695
|
+
request.field('appStoreId', data.appStoreId);
|
|
696
|
+
request.field('manifest', JSON.stringify(data.manifest));
|
|
697
|
+
request.field('location', data.location);
|
|
698
|
+
request.field('subdomain', data.subdomain);
|
|
699
|
+
request.field('domain', data.domain);
|
|
700
|
+
request.field('secondaryDomains', JSON.stringify(data.secondaryDomains));
|
|
701
|
+
request.field('aliasDomains', JSON.stringify(data.aliasDomains));
|
|
702
|
+
request.field('ports', JSON.stringify(data.ports));
|
|
703
|
+
request.field('accessRestriction', JSON.stringify(data.accessRestriction));
|
|
704
|
+
request.field('memoryLimit', String(data.memoryLimit));
|
|
705
|
+
request.field('env', JSON.stringify(data.env));
|
|
706
|
+
if (data.sso !== undefined) request.field('sso', String(data.sso));
|
|
707
|
+
if (data.debugMode) request.field('debugMode', JSON.stringify(data.debugMode));
|
|
708
|
+
if (data.icon) request.field('icon', data.icon);
|
|
709
|
+
request.attach('sourceArchive', sourceArchiveFilePath);
|
|
710
|
+
|
|
711
|
+
response = await request;
|
|
712
|
+
|
|
713
|
+
// Clean up temp file
|
|
714
|
+
safe.fs.unlinkSync(sourceArchiveFilePath);
|
|
715
|
+
} else {
|
|
716
|
+
response = await request.send(data);
|
|
717
|
+
}
|
|
718
|
+
|
|
694
719
|
if (response.status !== 202) return exit(`Failed to install app: ${requestError(response)}`);
|
|
695
720
|
|
|
696
721
|
const appId = response.body.id;
|
|
697
722
|
|
|
698
723
|
console.log('App is being installed.');
|
|
699
724
|
|
|
725
|
+
if (!options.wait) return;
|
|
726
|
+
|
|
700
727
|
await waitForFinishInstallation(appId, response.body.taskId, options);
|
|
701
728
|
console.log('\n\nApp is installed.');
|
|
702
729
|
} catch (error) {
|
|
@@ -766,30 +793,32 @@ async function setLocation(localOptions, cmd) {
|
|
|
766
793
|
|
|
767
794
|
// port bindings
|
|
768
795
|
if (options.portBindings) {
|
|
769
|
-
let
|
|
796
|
+
let userPorts;
|
|
770
797
|
// ask the user for port values if the ports are different in the app and the manifest
|
|
771
798
|
if (typeof options.portBindings === 'string') {
|
|
772
|
-
|
|
799
|
+
userPorts = {};
|
|
773
800
|
options.portBindings.split(',').forEach(function (kv) {
|
|
774
801
|
const tmp = kv.split('=');
|
|
775
802
|
if (isNaN(parseInt(tmp[1], 10))) return; // disable the port
|
|
776
|
-
|
|
803
|
+
userPorts[tmp[0]] = parseInt(tmp[1], 10);
|
|
777
804
|
});
|
|
778
805
|
} else {
|
|
779
|
-
|
|
806
|
+
userPorts = await queryPortBindings(app, app.manifest);
|
|
780
807
|
}
|
|
781
808
|
|
|
782
|
-
for (const port in
|
|
783
|
-
console.log('%s: %s', port,
|
|
809
|
+
for (const port in userPorts) {
|
|
810
|
+
console.log('%s: %s', port, userPorts[port]);
|
|
784
811
|
}
|
|
785
812
|
|
|
786
|
-
data.ports =
|
|
813
|
+
data.ports = userPorts;
|
|
787
814
|
}
|
|
788
815
|
|
|
789
816
|
const request = createRequest('POST', `/api/v1/apps/${app.id}/configure/location`, options);
|
|
790
817
|
const response = await request.send(data);
|
|
791
818
|
if (response.status !== 202) return exit(`Failed to configure app: ${requestError(response)}`);
|
|
792
819
|
|
|
820
|
+
if (!options.wait) return;
|
|
821
|
+
|
|
793
822
|
await waitForTask(response.body.taskId, options);
|
|
794
823
|
await waitForHealthy(app.id, options);
|
|
795
824
|
console.log('\n\nApp configured');
|
|
@@ -803,20 +832,37 @@ async function update(localOptions, cmd) {
|
|
|
803
832
|
|
|
804
833
|
try {
|
|
805
834
|
const app = await getApp(options);
|
|
806
|
-
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
807
835
|
|
|
808
836
|
const result = await getManifest(options.appstoreId || '');
|
|
809
837
|
const { manifest, manifestFilePath } = result;
|
|
810
838
|
|
|
811
839
|
let data = {};
|
|
840
|
+
let sourceArchiveFilePath = null; // will be set if we need to send a tarball
|
|
812
841
|
|
|
813
842
|
if (!manifest.dockerImage) { // not a manifest from appstore
|
|
814
843
|
const sourceDir = path.dirname(manifestFilePath);
|
|
815
|
-
const image = options.image || config.
|
|
844
|
+
const image = options.image || config.getAppBuildConfig(sourceDir).dockerImage;
|
|
816
845
|
|
|
817
|
-
if (!image)
|
|
846
|
+
if (!image) {
|
|
847
|
+
console.log('No docker image detected. Creating source archive from this folder.');
|
|
818
848
|
|
|
819
|
-
|
|
849
|
+
const dockerignoreFilePath = path.join(sourceDir, '.dockerignore');
|
|
850
|
+
const ignoreMatcher = dockerignoreMatcher(dockerignoreFilePath);
|
|
851
|
+
|
|
852
|
+
const tarStream = tar.pack(sourceDir, {
|
|
853
|
+
ignore: function (name) {
|
|
854
|
+
return ignoreMatcher(name.slice(sourceDir.length + 1)); // make name as relative path
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
sourceArchiveFilePath = path.join(os.tmpdir(), `cloudron-source-${Date.now()}.tar`);
|
|
859
|
+
const sourceArchiveStream = fs.createWriteStream(sourceArchiveFilePath);
|
|
860
|
+
|
|
861
|
+
const [tarError] = await safe(stream.pipeline(tarStream, sourceArchiveStream));
|
|
862
|
+
if (tarError) return exit(`Could not create source archive: ${tarError.message}`);
|
|
863
|
+
} else {
|
|
864
|
+
manifest.dockerImage = image;
|
|
865
|
+
}
|
|
820
866
|
|
|
821
867
|
if (manifest.icon) {
|
|
822
868
|
let iconFilename = manifest.icon.slice(0, 7) === 'file://' ? manifest.icon.slice(7) : manifest.icon;
|
|
@@ -826,24 +872,32 @@ async function update(localOptions, cmd) {
|
|
|
826
872
|
}
|
|
827
873
|
}
|
|
828
874
|
|
|
829
|
-
|
|
875
|
+
data = Object.assign(data, {
|
|
876
|
+
appStoreId: options.appstoreId || '', // note case change
|
|
877
|
+
manifest: manifest,
|
|
878
|
+
skipBackup: !options.backup,
|
|
879
|
+
skipNotification: true,
|
|
880
|
+
force: true // permit downgrades
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
const request = createRequest('POST', `/api/v1/apps/${app.id}/update`, options);
|
|
884
|
+
|
|
885
|
+
let response;
|
|
886
|
+
if (sourceArchiveFilePath) {
|
|
887
|
+
request.field('appStoreId', data.appStoreId);
|
|
888
|
+
request.field('manifest', JSON.stringify(data.manifest));
|
|
889
|
+
request.field('skipBackup', String(data.skipBackup));
|
|
890
|
+
request.field('skipNotification', String(data.skipNotification));
|
|
891
|
+
request.field('force', String(data.force));
|
|
892
|
+
if (data.icon) request.field('icon', data.icon);
|
|
893
|
+
request.attach('sourceArchive', sourceArchiveFilePath);
|
|
830
894
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
895
|
+
response = await request;
|
|
896
|
+
|
|
897
|
+
safe.fs.unlinkSync(sourceArchiveFilePath);
|
|
834
898
|
} else {
|
|
835
|
-
|
|
836
|
-
data = Object.assign(data, {
|
|
837
|
-
appStoreId: options.appstoreId || '', // note case change
|
|
838
|
-
manifest: manifest,
|
|
839
|
-
skipBackup: !options.backup,
|
|
840
|
-
skipNotification: true,
|
|
841
|
-
force: true // permit downgrades
|
|
842
|
-
});
|
|
899
|
+
response = await request.send(data);
|
|
843
900
|
}
|
|
844
|
-
|
|
845
|
-
const request = createRequest('POST', apiPath, options);
|
|
846
|
-
const response = await request.send(data);
|
|
847
901
|
if (response.status !== 202) return exit(`Failed to update app: ${requestError(response)}`);
|
|
848
902
|
|
|
849
903
|
process.stdout.write('\n => ' + 'Waiting for app to be updated ');
|
|
@@ -851,12 +905,11 @@ async function update(localOptions, cmd) {
|
|
|
851
905
|
await waitForFinishInstallation(app.id, response.body.taskId, options);
|
|
852
906
|
console.log('\n\nApp is updated.');
|
|
853
907
|
} catch (error) {
|
|
854
|
-
|
|
855
|
-
exit(`\n\nApp update error: ${error.message}`);
|
|
908
|
+
exit(`\nApp update error: ${error.message}`);
|
|
856
909
|
}
|
|
857
910
|
}
|
|
858
911
|
|
|
859
|
-
async function
|
|
912
|
+
async function debugApp(args, localOptions, cmd) {
|
|
860
913
|
try {
|
|
861
914
|
const options = cmd.optsWithGlobals();
|
|
862
915
|
const app = await getApp(options);
|
|
@@ -875,6 +928,8 @@ async function debug(args, localOptions, cmd) {
|
|
|
875
928
|
const response = await request.send(data);
|
|
876
929
|
if (response.status !== 202) return exit(`Failed to set debug mode: ${requestError(response)}`);
|
|
877
930
|
|
|
931
|
+
if (!options.wait) return;
|
|
932
|
+
|
|
878
933
|
await waitForTask(response.body.taskId, options);
|
|
879
934
|
|
|
880
935
|
const memoryLimit = options.limitMemory ? 0 : -1;
|
|
@@ -909,6 +964,8 @@ async function repair(localOptions, cmd) {
|
|
|
909
964
|
const response = await request.send(data);
|
|
910
965
|
if (response.status !== 202) return exit(`Failed to set repair mode: ${requestError(response)}`);
|
|
911
966
|
|
|
967
|
+
if (!options.wait) return;
|
|
968
|
+
|
|
912
969
|
process.stdout.write('\n => ' + 'Waiting for app to be repaired ');
|
|
913
970
|
|
|
914
971
|
await waitForFinishInstallation(app.id, response.body.taskId, options);
|
|
@@ -943,6 +1000,8 @@ async function uninstall(localOptions, cmd) {
|
|
|
943
1000
|
const response = await request.send({});
|
|
944
1001
|
if (response.status !== 202) return exit(`Failed to uninstall app: ${requestError(response)}`);
|
|
945
1002
|
|
|
1003
|
+
if (!options.wait) return;
|
|
1004
|
+
|
|
946
1005
|
process.stdout.write('\n => ' + 'Waiting for app to be uninstalled ');
|
|
947
1006
|
|
|
948
1007
|
await waitForTask(response.body.taskId, options);
|
|
@@ -1068,7 +1127,9 @@ async function restart(localOptions, cmd) {
|
|
|
1068
1127
|
const options = cmd.optsWithGlobals();
|
|
1069
1128
|
const app = await getApp(options);
|
|
1070
1129
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1071
|
-
await restartApp(app, options);
|
|
1130
|
+
const taskId = await restartApp(app, options);
|
|
1131
|
+
if (!options.wait) return;
|
|
1132
|
+
await waitForTask(taskId, options);
|
|
1072
1133
|
await waitForHealthy(app.id, options);
|
|
1073
1134
|
console.log('\n\nApp restarted');
|
|
1074
1135
|
} catch (error) {
|
|
@@ -1081,7 +1142,9 @@ async function start(localOptions, cmd) {
|
|
|
1081
1142
|
const options = cmd.optsWithGlobals();
|
|
1082
1143
|
const app = await getApp(options);
|
|
1083
1144
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1084
|
-
await startApp(app, options);
|
|
1145
|
+
const taskId = await startApp(app, options);
|
|
1146
|
+
if (!options.wait) return;
|
|
1147
|
+
await waitForTask(taskId, options);
|
|
1085
1148
|
await waitForHealthy(app.id, options);
|
|
1086
1149
|
console.log('\n\nApp started');
|
|
1087
1150
|
} catch (error) {
|
|
@@ -1094,7 +1157,9 @@ async function stop(localOptions, cmd) {
|
|
|
1094
1157
|
const options = cmd.optsWithGlobals();
|
|
1095
1158
|
const app = await getApp(options);
|
|
1096
1159
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1097
|
-
await stopApp(app, options);
|
|
1160
|
+
const taskId = await stopApp(app, options);
|
|
1161
|
+
if (!options.wait) return;
|
|
1162
|
+
await waitForTask(taskId, options);
|
|
1098
1163
|
console.log('\n\nApp stopped');
|
|
1099
1164
|
} catch (error) {
|
|
1100
1165
|
exit(error);
|
|
@@ -1120,6 +1185,8 @@ async function backupCreate(localOptions, cmd) {
|
|
|
1120
1185
|
const response = await request.send({ backupSiteId });
|
|
1121
1186
|
if (response.status !== 202) return exit(`Failed to start backup: ${requestError(response)}`);
|
|
1122
1187
|
|
|
1188
|
+
if (!options.wait) return;
|
|
1189
|
+
|
|
1123
1190
|
// FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
|
|
1124
1191
|
await waitForFinishBackup(app.id, response.body.taskId, options);
|
|
1125
1192
|
console.log('\n\nApp is backed up');
|
|
@@ -1319,21 +1386,21 @@ async function clone(localOptions, cmd) {
|
|
|
1319
1386
|
}
|
|
1320
1387
|
|
|
1321
1388
|
// taken from docker-modem
|
|
1322
|
-
function demuxStream(
|
|
1389
|
+
function demuxStream(inputStream, stdout, stderr) {
|
|
1323
1390
|
let header = null;
|
|
1324
1391
|
|
|
1325
|
-
|
|
1326
|
-
header = header ||
|
|
1392
|
+
inputStream.on('readable', function() {
|
|
1393
|
+
header = header || inputStream.read(8);
|
|
1327
1394
|
while (header !== null) {
|
|
1328
1395
|
const type = header.readUInt8(0);
|
|
1329
|
-
const payload =
|
|
1396
|
+
const payload = inputStream.read(header.readUInt32BE(4));
|
|
1330
1397
|
if (payload === null) break;
|
|
1331
1398
|
if (type == 2) {
|
|
1332
1399
|
stderr.write(payload);
|
|
1333
1400
|
} else {
|
|
1334
1401
|
stdout.write(payload);
|
|
1335
1402
|
}
|
|
1336
|
-
header =
|
|
1403
|
+
header = inputStream.read(8);
|
|
1337
1404
|
}
|
|
1338
1405
|
});
|
|
1339
1406
|
}
|
|
@@ -1344,7 +1411,7 @@ function demuxStream(stream, stdout, stderr) {
|
|
|
1344
1411
|
// echo "sauce" | cloudron exec -- bash -c "cat - > /app/data/sauce" - test with binary files. should disable tty
|
|
1345
1412
|
// cat ~/tmp/fantome.tar.gz | cloudron exec -- bash -c "tar xvf - -C /tmp" - must show an error
|
|
1346
1413
|
// cat ~/tmp/fantome.tar.gz | cloudron exec -- bash -c "tar zxf - -C /tmp" - must extrack ok
|
|
1347
|
-
async function
|
|
1414
|
+
async function execApp(args, localOptions, cmd) {
|
|
1348
1415
|
let stdin = localOptions._stdin || process.stdin; // hack for 'push', 'pull' to reuse this function
|
|
1349
1416
|
const stdout = localOptions._stdout || process.stdout; // hack for 'push', 'pull' to reuse this function
|
|
1350
1417
|
|
|
@@ -1378,12 +1445,12 @@ async function exec(args, localOptions, cmd) {
|
|
|
1378
1445
|
process.exit(response2.body.exitCode);
|
|
1379
1446
|
}
|
|
1380
1447
|
|
|
1381
|
-
const { adminFqdn, token, rejectUnauthorized } = requestOptions(cmd.optsWithGlobals());
|
|
1448
|
+
const { adminFqdn, token: reqToken, rejectUnauthorized } = requestOptions(cmd.optsWithGlobals());
|
|
1382
1449
|
|
|
1383
1450
|
const searchParams = new URLSearchParams({
|
|
1384
1451
|
rows: stdout.rows || 24,
|
|
1385
1452
|
columns: stdout.columns || 80,
|
|
1386
|
-
access_token:
|
|
1453
|
+
access_token: reqToken,
|
|
1387
1454
|
tty
|
|
1388
1455
|
});
|
|
1389
1456
|
|
|
@@ -1448,7 +1515,7 @@ async function exec(args, localOptions, cmd) {
|
|
|
1448
1515
|
req.end(); // this makes the request
|
|
1449
1516
|
}
|
|
1450
1517
|
|
|
1451
|
-
function
|
|
1518
|
+
function pushApp(localDir, remote, localOptions, cmd) {
|
|
1452
1519
|
// deal with paths prefixed with ~
|
|
1453
1520
|
const local = localDir.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
1454
1521
|
const stat = fs.existsSync(path.resolve(local)) ? fs.lstatSync(local) : null;
|
|
@@ -1461,7 +1528,7 @@ function push(localDir, remote, localOptions, cmd) {
|
|
|
1461
1528
|
return tarzip.stdout;
|
|
1462
1529
|
};
|
|
1463
1530
|
|
|
1464
|
-
|
|
1531
|
+
execApp(['tar', 'zxvf', '-', '-C', remote], localOptions, cmd);
|
|
1465
1532
|
} else {
|
|
1466
1533
|
if (local === '-') {
|
|
1467
1534
|
localOptions._stdin = process.stdin;
|
|
@@ -1477,7 +1544,7 @@ function push(localDir, remote, localOptions, cmd) {
|
|
|
1477
1544
|
remote = remote + '/' + path.basename(local); // do not use path.join as we want this to be a UNIX path
|
|
1478
1545
|
}
|
|
1479
1546
|
|
|
1480
|
-
|
|
1547
|
+
execApp(['bash', '-c', `cat - > "${remote}"`], localOptions, cmd);
|
|
1481
1548
|
}
|
|
1482
1549
|
}
|
|
1483
1550
|
|
|
@@ -1489,7 +1556,7 @@ function pull(remote, local, localOptions, cmd) {
|
|
|
1489
1556
|
unzip.pipe(untar);
|
|
1490
1557
|
localOptions._stdout = unzip;
|
|
1491
1558
|
|
|
1492
|
-
|
|
1559
|
+
execApp(['tar', 'zcf', '-', '-C', remote, '.'], localOptions, cmd);
|
|
1493
1560
|
} else {
|
|
1494
1561
|
if (fs.existsSync(local) && fs.lstatSync(local).isDirectory()) {
|
|
1495
1562
|
local = path.join(local, path.basename(remote));
|
|
@@ -1502,7 +1569,7 @@ function pull(remote, local, localOptions, cmd) {
|
|
|
1502
1569
|
|
|
1503
1570
|
localOptions._stdout.on('error', function (error) { exit('Error pulling', error); });
|
|
1504
1571
|
|
|
1505
|
-
|
|
1572
|
+
execApp(['cat', remote], localOptions, cmd);
|
|
1506
1573
|
}
|
|
1507
1574
|
}
|
|
1508
1575
|
|
|
@@ -1513,10 +1580,10 @@ function init(localOptions, cmd) {
|
|
|
1513
1580
|
|
|
1514
1581
|
const manifestTemplateFilename = options.appstore ? 'CloudronManifest.appstore.json.ejs' : 'CloudronManifest.json.ejs';
|
|
1515
1582
|
|
|
1516
|
-
const manifestTemplate = fs.readFileSync(path.join(
|
|
1517
|
-
const dockerfileTemplate = fs.readFileSync(path.join(
|
|
1518
|
-
const dockerignoreTemplate = fs.readFileSync(path.join(
|
|
1519
|
-
const startShTemplate = fs.readFileSync(path.join(
|
|
1583
|
+
const manifestTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/', manifestTemplateFilename), 'utf8');
|
|
1584
|
+
const dockerfileTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/', 'Dockerfile.ejs'), 'utf8');
|
|
1585
|
+
const dockerignoreTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/', 'dockerignore.ejs'), 'utf8');
|
|
1586
|
+
const startShTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/', 'start.sh.ejs'), 'utf8');
|
|
1520
1587
|
|
|
1521
1588
|
const data = {
|
|
1522
1589
|
version: '0.1.0',
|
|
@@ -1549,7 +1616,7 @@ function init(localOptions, cmd) {
|
|
|
1549
1616
|
fs.chmodSync('start.sh', 0o0775);
|
|
1550
1617
|
}
|
|
1551
1618
|
|
|
1552
|
-
fs.copyFileSync(path.join(
|
|
1619
|
+
fs.copyFileSync(path.join(import.meta.dirname, 'templates/', 'logo.png'), 'logo.png');
|
|
1553
1620
|
|
|
1554
1621
|
if (options.appstore) {
|
|
1555
1622
|
if (!fs.existsSync('.gitignore')) fs.writeFileSync('.gitignore', 'node_modules/\n', 'utf8');
|
|
@@ -1658,3 +1725,38 @@ async function envGet(envName, localOptions, cmd) {
|
|
|
1658
1725
|
exit(error);
|
|
1659
1726
|
}
|
|
1660
1727
|
}
|
|
1728
|
+
|
|
1729
|
+
export default {
|
|
1730
|
+
list,
|
|
1731
|
+
login,
|
|
1732
|
+
logout,
|
|
1733
|
+
open: openApp,
|
|
1734
|
+
install,
|
|
1735
|
+
setLocation,
|
|
1736
|
+
debug: debugApp,
|
|
1737
|
+
update,
|
|
1738
|
+
uninstall,
|
|
1739
|
+
logs,
|
|
1740
|
+
exec: execApp,
|
|
1741
|
+
status,
|
|
1742
|
+
inspect,
|
|
1743
|
+
pull,
|
|
1744
|
+
push: pushApp,
|
|
1745
|
+
restart,
|
|
1746
|
+
start,
|
|
1747
|
+
stop,
|
|
1748
|
+
repair,
|
|
1749
|
+
init,
|
|
1750
|
+
restore,
|
|
1751
|
+
importApp,
|
|
1752
|
+
exportApp,
|
|
1753
|
+
clone,
|
|
1754
|
+
backupCreate,
|
|
1755
|
+
backupList,
|
|
1756
|
+
cancel,
|
|
1757
|
+
|
|
1758
|
+
envGet,
|
|
1759
|
+
envSet,
|
|
1760
|
+
envList,
|
|
1761
|
+
envUnset
|
|
1762
|
+
};
|