cloudron 4.12.10 → 4.14.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.
package/src/actions.js CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const assert = require('assert'),
4
- async = require('async'),
5
4
  config = require('./config.js'),
5
+ delay = require('delay'),
6
6
  ejs = require('ejs'),
7
7
  EventSource = require('eventsource'),
8
8
  fs = require('fs'),
@@ -16,19 +16,15 @@ const assert = require('assert'),
16
16
  ProgressStream = require('progress-stream'),
17
17
  querystring = require('querystring'),
18
18
  readlineSync = require('readline-sync'),
19
- request = require('request'),
20
19
  safe = require('safetydance'),
21
20
  spawn = require('child_process').spawn,
22
21
  split = require('split'),
23
22
  superagent = require('superagent'),
24
23
  Table = require('easy-table'),
25
24
  tar = require('tar-fs'),
26
- util = require('util'),
27
25
  zlib = require('zlib'),
28
26
  _ = require('underscore');
29
27
 
30
- require('colors');
31
-
32
28
  const exit = helper.exit;
33
29
 
34
30
  exports = module.exports = {
@@ -51,7 +47,6 @@ exports = module.exports = {
51
47
  start,
52
48
  stop,
53
49
  repair,
54
- createOAuthAppCredentials,
55
50
  init,
56
51
  restore,
57
52
  importApp,
@@ -80,6 +75,18 @@ function requestOptions(options) {
80
75
  return { adminFqdn, token, rejectUnauthorized };
81
76
  }
82
77
 
78
+ function createRequest(method, apiPath, options) {
79
+ const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
80
+
81
+ let url = `https://${adminFqdn}${apiPath}`;
82
+ if (url.includes('?')) url += '&'; else url += '?';
83
+ url += `access_token=${token}`;
84
+ const request = superagent(method, url);
85
+ if (rejectUnauthorized) request.disableTLSCerts();
86
+ request.ok(() => true);
87
+ return request;
88
+ }
89
+
83
90
  // error for the request module
84
91
  function requestError(response) {
85
92
  if (response.statusCode === 401) return 'Invalid token. Use cloudron login again.';
@@ -87,166 +94,231 @@ function requestError(response) {
87
94
  return `${response.statusCode} message: ${response.body.message || response.body}`; // body is sometimes just a string like in 401
88
95
  }
89
96
 
90
- function selectDomain(location, options, callback) {
97
+ async function selectDomain(location, options) {
91
98
  assert.strictEqual(typeof location, 'string');
92
99
  assert.strictEqual(typeof options, 'object');
93
- assert.strictEqual(typeof callback, 'function');
94
100
 
95
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
101
+ const { adminFqdn } = requestOptions(options);
96
102
 
97
- request.get(`https://${adminFqdn}/api/v1/domains?access_token=${token}`, { rejectUnauthorized, json: true }, function (error, response) {
98
- if (error) return callback(error);
99
- if (response.statusCode !== 200) return callback(new Error(`Failed to list domains: ${requestError(response)}`));
103
+ const response = await createRequest('GET', '/api/v1/domains', options);
104
+ if (response.statusCode !== 200) throw new Error(`Failed to list domains: ${requestError(response)}`);
100
105
 
101
- const domains = response.body.domains;
106
+ const domains = response.body.domains;
102
107
 
103
- let domain;
104
- let matchingDomain = domains
108
+ let domain;
109
+ let subdomain = location;
110
+ let matchingDomain = domains
111
+ .map(function (d) { return d.domain; } )
112
+ .sort(function(a, b) { return a.length < b.length; })
113
+ .find(function (d) { return location.endsWith(d); });
114
+
115
+ if (matchingDomain) {
116
+ domain = matchingDomain;
117
+ subdomain = location.slice(0, -matchingDomain.length-1);
118
+ } else { // use the admin domain
119
+ domain = domains
105
120
  .map(function (d) { return d.domain; } )
106
121
  .sort(function(a, b) { return a.length < b.length; })
107
- .find(function (d) { return location.endsWith(d); });
108
-
109
- if (matchingDomain) {
110
- domain = matchingDomain;
111
- location = location.slice(0, -matchingDomain.length-1);
112
- } else { // use the admin domain
113
- domain = domains
114
- .map(function (d) { return d.domain; } )
115
- .sort(function(a, b) { return a.length < b.length; })
116
- .find(function (d) { return adminFqdn.endsWith(d); });
117
- }
122
+ .find(function (d) { return adminFqdn.endsWith(d); });
123
+ }
118
124
 
119
- callback(null, { location, domain });
120
- });
125
+ return { subdomain, domain };
121
126
  }
122
127
 
123
- function stopActiveTask(app, options, callback) {
128
+ async function stopActiveTask(app, options) {
124
129
  assert.strictEqual(typeof app, 'object');
125
130
  assert.strictEqual(typeof options, 'object');
126
- assert.strictEqual(typeof callback, 'function');
127
131
 
128
- if (!app.taskId) return callback();
132
+ if (!app.taskId) return;
129
133
 
130
134
  console.log(`Stopping app's current active task ${app.taskId}`);
131
135
 
132
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
133
-
134
- request.post(`https://${adminFqdn}/api/v1/tasks/${app.taskId}/stop?access_token=${token}`, { json: {}, rejectUnauthorized }, function (error, response) {
135
- if (error) return callback(error);
136
- if (response.statusCode !== 204) return exit(`Failed to stop active task: ${requestError(response)}`);
137
-
138
- callback();
139
- });
136
+ const response = await createRequest('POST', `/api/v1/tasks/${app.taskId}/stop`, options);
137
+ if (response.statusCode !== 204) throw `Failed to stop active task: ${requestError(response)}`;
140
138
  }
141
139
 
142
- function selectAppWithRepository(repository, options, callback) {
140
+ async function selectAppWithRepository(repository, options) {
143
141
  assert.strictEqual(typeof repository, 'string');
144
142
  assert.strictEqual(typeof options, 'object');
145
- assert.strictEqual(typeof callback, 'function');
146
-
147
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
148
143
 
149
- request.get(`https://${adminFqdn}/api/v1/apps?access_token=${token}`, { rejectUnauthorized, json: true }, function (error, response) {
150
- if (error) return callback(error);
151
- if (response.statusCode !== 200) return callback(new Error(`Failed to install app: ${requestError(response)}`));
144
+ const response = await createRequest('GET', '/api/v1/apps', options);
145
+ if (response.statusCode !== 200) throw new Error(`Failed to install app: ${requestError(response)}`);
152
146
 
153
- let matchingApps = response.body.apps.filter(function (app) {
154
- return !app.appStoreId && app.manifest.dockerImage.startsWith(repository); // never select apps from the store
155
- });
147
+ let matchingApps = response.body.apps.filter(function (app) {
148
+ return !app.appStoreId && app.manifest.dockerImage.startsWith(repository); // never select apps from the store
149
+ });
156
150
 
157
- if (matchingApps.length === 0) return callback(null, [ ]);
158
- if (matchingApps.length === 1) return callback(null, matchingApps[0]);
151
+ if (matchingApps.length === 0) return [ ];
152
+ if (matchingApps.length === 1) return matchingApps[0];
159
153
 
160
- console.log();
161
- console.log('Available apps using same repository %s:', repository);
162
- matchingApps.sort(function (a, b) { return a.fqdn < b.fqdn ? -1 : 1; });
163
- matchingApps.forEach(function (app, index) {
164
- console.log('[%s]\t%s', index, app.fqdn);
165
- });
154
+ console.log();
155
+ console.log('Available apps using same repository %s:', repository);
156
+ matchingApps.sort(function (a, b) { return a.fqdn < b.fqdn ? -1 : 1; });
157
+ matchingApps.forEach(function (app, index) {
158
+ console.log('[%s]\t%s', index, app.fqdn);
159
+ });
166
160
 
167
- var index = -1;
168
- // eslint-disable-next-line no-constant-condition
169
- while (true) {
170
- index = parseInt(readlineSync.question('Choose app [0-' + (matchingApps.length-1) + ']: ', {}), 10);
171
- if (isNaN(index) || index < 0 || index > matchingApps.length-1) console.log('Invalid selection'.red);
172
- else break;
173
- }
161
+ let index = -1;
162
+ // eslint-disable-next-line no-constant-condition
163
+ while (true) {
164
+ index = parseInt(readlineSync.question('Choose app [0-' + (matchingApps.length-1) + ']: ', {}), 10);
165
+ if (isNaN(index) || index < 0 || index > matchingApps.length-1) console.log('Invalid selection');
166
+ else break;
167
+ }
174
168
 
175
- callback(null, matchingApps[index]);
176
- });
169
+ return matchingApps[index];
177
170
  }
178
171
 
179
172
  // appId may be the appId or the location
180
- function getApp(options, callback) {
173
+ async function getApp(options) {
181
174
  assert.strictEqual(typeof options, 'object');
182
- assert.strictEqual(typeof callback, 'function');
183
175
 
184
176
  const app = options.app || null;
185
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
186
177
 
187
178
  if (!app) { // determine based on repository name given during 'build'
188
179
  const manifestFilePath = helper.locateManifest();
189
180
 
190
- if (!manifestFilePath) return callback(new Error(NO_APP_FOUND_ERROR_STRING));
181
+ if (!manifestFilePath) throw new Error(NO_APP_FOUND_ERROR_STRING);
191
182
 
192
183
  const sourceDir = path.dirname(manifestFilePath);
193
184
  const appConfig = config.getAppConfig(sourceDir);
194
185
 
195
- if (!appConfig.repository) return callback(new Error(NO_APP_FOUND_ERROR_STRING));
186
+ if (!appConfig.repository) throw new Error(NO_APP_FOUND_ERROR_STRING);
196
187
 
197
- selectAppWithRepository(appConfig.repository, options, function (error, result) {
198
- if (error || result.length === 0) return callback(null, null);
188
+ const [error, result] = await safe(selectAppWithRepository(appConfig.repository, options));
189
+ if (error || result.length === 0) return null;
199
190
 
200
- callback(null, result);
201
- });
191
+ return result;
202
192
  } else if (app.match(/.{8}-.{4}-.{4}-.{4}-.{8}/)) { // it is an id
203
- request.get(`https://${adminFqdn}/api/v1/apps/${app}?access_token=${token}`, { rejectUnauthorized, json: true }, function (error, response) {
204
- if (error) return callback(error);
205
- if (response.statusCode !== 200) return callback(new Error(`Failed to get app: ${requestError(response)}`));
193
+ const response = await createRequest('GET', `/api/v1/apps/${app}`, options);
194
+ if (response.statusCode !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
206
195
 
207
- callback(null, response.body);
208
- });
196
+ return response.body;
209
197
  } else { // it is a location
210
- request.get(`https://${adminFqdn}/api/v1/apps?access_token=${token}`, { rejectUnauthorized, json: true }, function (error, response) {
211
- if (error) return callback(error);
212
- if (response.statusCode !== 200) return callback(new Error(`Failed to get apps: ${requestError(response)}`));
198
+ const response = await createRequest('GET', '/api/v1/apps', options);
199
+ if (response.statusCode !== 200) throw new Error(`Failed to get apps: ${requestError(response)}`);
213
200
 
214
- let match = response.body.apps.filter(function (m) { return m.location === app || m.fqdn === app; });
215
- if (match.length == 0) return callback(new Error(`App at location ${app} not found`));
201
+ const match = response.body.apps.filter(function (m) { return m.subdomain === app || m.location === app || m.fqdn === app; });
202
+ if (match.length == 0) throw new Error(`App at location ${app} not found`);
216
203
 
217
- callback(null, match[0]);
218
- });
204
+ return match[0];
219
205
  }
220
206
  }
221
207
 
222
- function stopApp(app, options, callback) {
223
- assert.strictEqual(typeof app, 'object');
208
+ async function waitForHealthy(appId, options) {
209
+ assert.strictEqual(typeof appId, 'string');
224
210
  assert.strictEqual(typeof options, 'object');
225
- assert.strictEqual(typeof callback, 'function');
226
211
 
227
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
212
+ if (!options.wait) return;
228
213
 
229
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/stop?access_token=${token}`, { json: {}, rejectUnauthorized }, function (error, response) {
230
- if (error) return callback(error);
231
- if (response.statusCode !== 202) return callback(`Failed to stop app: ${requestError(response)}`);
214
+ process.stdout.write('\n => ' + 'Wait for health check ');
232
215
 
233
- waitForTask(response.body.taskId, options, callback);
234
- });
216
+ // eslint-disable-next-line no-constant-condition
217
+ while (true) {
218
+ await delay(1000);
219
+ const response = await createRequest('GET', `/api/v1/apps/${appId}`, options);
220
+ if (response.statusCode !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
221
+
222
+ // do not check installation state here. it can be pending_backup etc (this is a bug in box code)
223
+ if (response.body.health === 'healthy') return;
224
+
225
+ // skip health check if app is in debug mode
226
+ if (response.body.debugMode !== null) {
227
+ process.stdout.write(' (skipped in debug mode)');
228
+ return;
229
+ }
230
+
231
+ process.stdout.write('.');
232
+ }
233
+ }
234
+
235
+ async function waitForTask(taskId, options) {
236
+ assert.strictEqual(typeof taskId, 'string');
237
+ assert.strictEqual(typeof options, 'object');
238
+
239
+ let currentMessage = '';
240
+
241
+ // eslint-disable-next-line no-constant-condition
242
+ while (true) {
243
+ const response = await createRequest('GET', `/api/v1/tasks/${taskId}`, options);
244
+ if (response.statusCode !== 200) throw new Error(`Failed to get task: ${requestError(response)}`);
245
+
246
+ // TODO remove later, for now keep old behavior on if `pending` is missing
247
+ if (typeof response.body.pending === 'undefined') {
248
+ // note: for queued tasks, 'active' returns false
249
+ if (response.body.error || response.body.percent === 100) return response.body; // task errored or done
250
+ } else {
251
+ if (!response.body.pending && !response.body.active) return response.body;
252
+ }
253
+
254
+ let message = response.body.message || '';
255
+
256
+ // Backup downloading includes the filename and clutters the cli output
257
+ if (message.indexOf('Restore - Down') === 0) {
258
+ message = 'Downloading backup';
259
+ }
260
+
261
+ // track current progress and show progress dots
262
+ if (currentMessage && currentMessage === message) {
263
+ if (currentMessage.indexOf('Creating image') === -1 && currentMessage.indexOf('Downloading backup') === -1) process.stdout.write('.');
264
+ } else if (message) {
265
+ process.stdout.write('\n => ' + message.trim() + ' ');
266
+ } else {
267
+ process.stdout.write('\n => ' + 'Waiting to start installation ');
268
+ }
269
+
270
+ currentMessage = message;
271
+
272
+ await delay(1000);
273
+ }
235
274
  }
236
275
 
237
- function startApp(app, options, callback) {
276
+ async function waitForFinishInstallation(appId, taskId, options) {
277
+ assert.strictEqual(typeof appId, 'string');
278
+ assert.strictEqual(typeof taskId, 'string');
279
+ assert.strictEqual(typeof options, 'object');
280
+
281
+ await waitForTask(taskId, options);
282
+
283
+ const response = await createRequest('GET', `/api/v1/apps/${appId}`, options);
284
+ if (response.statusCode !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
285
+
286
+ if (response.body.installationState !== 'installed') throw new Error(`Installation failed: ${response.body.error ? response.body.error.message : ''}`);
287
+
288
+ await waitForHealthy(appId, options);
289
+ }
290
+
291
+ async function waitForFinishBackup(appId, taskId, options) {
292
+ assert.strictEqual(typeof appId, 'string');
293
+ assert.strictEqual(typeof taskId, 'string');
294
+ assert.strictEqual(typeof options, 'object');
295
+
296
+ const result = await waitForTask(taskId, options);
297
+
298
+ if (result.error) throw new Error(`Backup failed: ${result.error.message}`);
299
+
300
+ const response = await createRequest('GET', `/api/v1/apps/${appId}`, options);
301
+ if (response.statusCode !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
302
+ }
303
+
304
+ async function stopApp(app, options) {
238
305
  assert.strictEqual(typeof app, 'object');
239
306
  assert.strictEqual(typeof options, 'object');
240
- assert.strictEqual(typeof callback, 'function');
241
307
 
242
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
308
+ const response = await createRequest('POST', `/api/v1/apps/${app.id}/stop`, options);
309
+ if (response.statusCode !== 202) throw `Failed to stop app: ${requestError(response)}`;
243
310
 
244
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/start?access_token=${token}`, { json: {}, rejectUnauthorized }, function (error, response) {
245
- if (error) return callback(error);
246
- if (response.statusCode !== 202) return callback(`Failed to stop app: ${requestError(response)}`);
311
+ await waitForTask(response.body.taskId, options);
312
+ }
247
313
 
248
- waitForTask(response.body.taskId, options, callback);
249
- });
314
+ async function startApp(app, options) {
315
+ assert.strictEqual(typeof app, 'object');
316
+ assert.strictEqual(typeof options, 'object');
317
+
318
+ const response = await createRequest('POST', `/api/v1/apps/${app.id}/start`, options);
319
+ if (response.statusCode !== 202) throw `Failed to start app: ${requestError(response)}`;
320
+
321
+ await waitForTask(response.body.taskId, options);
250
322
  }
251
323
 
252
324
  async function authenticate(adminFqdn, username, password, options) {
@@ -271,7 +343,7 @@ async function authenticate(adminFqdn, username, password, options) {
271
343
  if (response.body.message === 'A totpToken must be provided') {
272
344
  return await authenticate(adminFqdn, username, password, { rejectUnauthorized, askForTotpToken: true });
273
345
  } else if (response.body.message === 'Invalid totpToken') {
274
- console.log('Invalid 2FA Token'.red);
346
+ console.log('Invalid 2FA Token');
275
347
  return await authenticate(adminFqdn, username, password, { rejectUnauthorized, askForTotpToken: true });
276
348
  } else {
277
349
  throw new Error('Invalid credentials');
@@ -299,10 +371,10 @@ async function login(adminFqdn, options) {
299
371
  .ok(() => true));
300
372
  if (error) return exit(error);
301
373
  if (response.status === 200) {
302
- console.log('Existing token still valid.'.green);
374
+ console.log('Existing token still valid.');
303
375
  } else {
304
376
  token = null;
305
- console.log(`Existing token possibly expired: ${requestError(response)}`.red);
377
+ console.log(`Existing token possibly expired: ${requestError(response)}`);
306
378
  }
307
379
  }
308
380
 
@@ -322,7 +394,7 @@ async function login(adminFqdn, options) {
322
394
  config.setAllowSelfsigned(options.parent.allowSelfsigned || options.parent.acceptSelfsigned);
323
395
  config.set('cloudrons.default', adminFqdn);
324
396
 
325
- console.log('Login successful.'.green);
397
+ console.log('Login successful.');
326
398
  }
327
399
 
328
400
  function logout() {
@@ -332,187 +404,70 @@ function logout() {
332
404
  console.log('Logged out.');
333
405
  }
334
406
 
335
- function open(options) {
336
- getApp(options, function (error, app) {
337
- if (error) return exit(error);
407
+ async function open(options) {
408
+ const [error, app] = await safe(getApp(options));
409
+ if (error) return exit(error);
338
410
 
339
- if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
411
+ if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
340
412
 
341
- opn(`https://${app.fqdn}`);
342
- });
413
+ opn(`https://${app.fqdn}`);
343
414
  }
344
415
 
345
- function list(options) {
416
+ async function list(options) {
346
417
  helper.verifyArguments(arguments);
347
418
 
348
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
419
+ const [error, response] = await safe(createRequest('GET', '/api/v1/apps', options));
420
+ if (error) return exit(error);
421
+ if (response.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
349
422
 
350
- request.get(`https://${adminFqdn}/api/v1/apps?access_token=${token}`, { rejectUnauthorized, json: true }, function (error, response) {
351
- if (error) return exit(error);
352
- if (response.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
423
+ if (response.body.apps.length === 0) return console.log('No apps installed.');
353
424
 
354
- if (response.body.apps.length === 0) return console.log('No apps installed.');
425
+ if (options.quiet) {
426
+ console.log(response.body.apps.map(a => a.id).join('\n'));
427
+ return;
428
+ }
355
429
 
356
- if (options.quiet) {
357
- console.log(response.body.apps.map(a => a.id).join('\n'));
358
- return;
430
+ var t = new Table();
431
+
432
+ response.body.apps.forEach(function (app) {
433
+ t.cell('Id', app.id);
434
+ t.cell('Location', app.fqdn);
435
+ t.cell('Manifest Id', (app.manifest.id || 'customapp') + '@' + app.manifest.version);
436
+ var prettyState;
437
+ if (app.installationState === 'installed') {
438
+ prettyState = (app.debugMode ? 'debug' : app.runState);
439
+ } else if (app.installationState === 'error') {
440
+ prettyState = `error (${app.error.installationState})`;
441
+ } else {
442
+ prettyState = app.installationState;
359
443
  }
360
-
361
- var t = new Table();
362
-
363
- response.body.apps.forEach(function (app) {
364
- t.cell('Id', app.id);
365
- t.cell('Location', app.fqdn);
366
- t.cell('Manifest Id', (app.manifest.id || 'customapp') + '@' + app.manifest.version);
367
- var prettyState;
368
- if (app.installationState === 'installed') {
369
- prettyState = (app.debugMode ? 'debug' : app.runState);
370
- } else if (app.installationState === 'error') {
371
- prettyState = `error (${app.error.installationState})`;
372
- } else {
373
- prettyState = app.installationState;
374
- }
375
- t.cell('State', prettyState);
376
- t.newRow();
377
- });
378
-
379
- console.log();
380
- console.log(t.toString());
444
+ t.cell('State', prettyState);
445
+ t.newRow();
381
446
  });
382
- }
383
-
384
- function waitForHealthy(appId, options, callback) {
385
- assert.strictEqual(typeof appId, 'string');
386
- assert.strictEqual(typeof options, 'object');
387
- assert.strictEqual(typeof callback, 'function');
388
-
389
- if (!options.wait) return callback();
390
447
 
391
- process.stdout.write('\n => ' + 'Wait for health check '.cyan);
392
-
393
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
394
-
395
- function checkStatus() {
396
- request.get(`https://${adminFqdn}/api/v1/apps/${appId}?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
397
- if (error) return callback(error);
398
- if (response.statusCode !== 200) return callback(new Error(`Failed to get app: ${requestError(response)}`));
399
-
400
- // do not check installation state here. it can be pending_backup etc (this is a bug in box code)
401
- if (response.body.health === 'healthy') return callback();
402
-
403
- // skip health check if app is in debug mode
404
- if (response.body.debugMode !== null) {
405
- process.stdout.write(' (skipped in debug mode)');
406
- return callback();
407
- }
408
-
409
- process.stdout.write('.');
410
-
411
- return setTimeout(checkStatus, 1000);
412
- });
413
- }
414
-
415
- setTimeout(checkStatus, 1000);
448
+ console.log();
449
+ console.log(t.toString());
416
450
  }
417
451
 
418
- function waitForTask(taskId, options, callback) {
419
- assert.strictEqual(typeof taskId, 'string');
420
- assert.strictEqual(typeof options, 'object');
421
- assert.strictEqual(typeof callback, 'function');
422
-
423
- let currentMessage = '';
424
-
425
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
426
-
427
- function checkStatus() {
428
- request.get(`https://${adminFqdn}/api/v1/tasks/${taskId}?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
429
- if (error) return callback(error);
430
- if (response.statusCode !== 200) return callback(new Error(`Failed to get task: ${requestError(response)}`));
431
-
432
- // TODO remove later, for now keep old behavior on if `pending` is missing
433
- if (typeof response.body.pending === 'undefined') {
434
- // note: for queued tasks, 'active' returns false
435
- if (response.body.error || response.body.percent === 100) return callback(null, response.body); // task errored or done
436
- } else {
437
- if (!response.body.pending && !response.body.active) return callback(null, response.body);
438
- }
439
-
452
+ async function querySecondaryDomains(app, manifest, options) {
453
+ const secondaryDomains = {};
440
454
 
441
- let message = response.body.message || '';
455
+ if(!manifest.httpPorts) return secondaryDomains;
442
456
 
443
- // Backup downloading includes the filename and clutters the cli output
444
- if (message.indexOf('Restore - Down') === 0) {
445
- message = 'Downloading backup';
446
- }
447
-
448
- // track current progress and show progress dots
449
- if (currentMessage && currentMessage === message) {
450
- if (currentMessage.indexOf('Creating image') === -1 && currentMessage.indexOf('Downloading backup') === -1) process.stdout.write('.');
451
- } else if (message) {
452
- process.stdout.write('\n => ' + message.trim().cyan + ' ');
453
- } else {
454
- process.stdout.write('\n => ' + 'Waiting to start installation '.cyan);
455
- }
456
-
457
- currentMessage = message;
458
-
459
- setTimeout(checkStatus, 1000);
460
- });
457
+ for (const env in manifest.httpPorts) {
458
+ const defaultDomain = (app && app.secondaryDomains && app.secondaryDomains[env]) ? app.secondaryDomains[env] : (manifest.httpPorts[env].defaultValue || '');
459
+ const input = readlineSync.question(`${manifest.httpPorts[env].description} (default: "${defaultDomain}"): `, {});
460
+ secondaryDomains[env] = await selectDomain(input, options);
461
461
  }
462
-
463
- checkStatus();
464
- }
465
-
466
- function waitForFinishInstallation(appId, taskId, options, callback) {
467
- assert.strictEqual(typeof appId, 'string');
468
- assert.strictEqual(typeof taskId, 'string');
469
- assert.strictEqual(typeof options, 'object');
470
- assert.strictEqual(typeof callback, 'function');
471
-
472
- waitForTask(taskId, options, function (error) {
473
- if (error) return callback(error);
474
-
475
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
476
-
477
- request.get(`https://${adminFqdn}/api/v1/apps/${appId}?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
478
- if (error) return callback(error);
479
- if (response.statusCode !== 200) return callback(new Error(`Failed to get app: ${requestError(response)}`));
480
-
481
- if (response.body.installationState !== 'installed') return callback(new Error(`Installation failed: ${response.body.error ? response.body.error.message : ''}`));
482
-
483
- waitForHealthy(appId, options, callback);
484
- });
485
- });
486
- }
487
-
488
- function waitForFinishBackup(appId, taskId, options, callback) {
489
- assert.strictEqual(typeof appId, 'string');
490
- assert.strictEqual(typeof taskId, 'string');
491
- assert.strictEqual(typeof options, 'object');
492
- assert.strictEqual(typeof callback, 'function');
493
-
494
- waitForTask(taskId, options, function (error, result) {
495
- if (error) return callback(error);
496
-
497
- if (result.error) return callback(new Error(`Backup failed: ${result.error.message}`));
498
-
499
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
500
-
501
- request.get(`https://${adminFqdn}/api/v1/apps/${appId}?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
502
- if (error) return callback(error);
503
- if (response.statusCode !== 200) return callback(new Error(`Failed to get app: ${requestError(response)}`));
504
-
505
- callback();
506
- });
507
- });
462
+ return secondaryDomains;
508
463
  }
509
464
 
510
465
  function queryPortBindings(app, manifest) {
511
- var portBindings = { };
466
+ const portBindings = {};
512
467
  const allPorts = _.extend({}, manifest.tcpPorts, manifest.udpPorts);
513
- for (var env in allPorts) {
514
- var defaultPort = (app && app.portBindings && app.portBindings[env]) ? app.portBindings[env] : (allPorts[env].defaultValue || '');
515
- var port = readlineSync.question(allPorts[env].description + ' (default ' + env + '=' + defaultPort + '. "x" to disable): ', {});
468
+ for (const env in allPorts) {
469
+ const defaultPort = (app && app.portBindings && app.portBindings[env]) ? app.portBindings[env] : (allPorts[env].defaultValue || '');
470
+ const port = readlineSync.question(allPorts[env].description + ' (default ' + env + '=' + defaultPort + '. "x" to disable): ', {});
516
471
  if (port === '') {
517
472
  portBindings[env] = defaultPort;
518
473
  } else if (isNaN(parseInt(port, 10))) {
@@ -534,55 +489,52 @@ function parseDebugCommand(cmd) {
534
489
  return cmd.split(' '); // yet another hack
535
490
  }
536
491
 
537
- function downloadManifest(appstoreId, callback) {
492
+ async function downloadManifest(appstoreId) {
538
493
  const parts = appstoreId.split('@');
539
494
  const url = config.appStoreOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
540
495
 
541
- request.get(url, { json: true }, function (error, response) {
542
- if (error) return callback(error);
543
- if (response.statusCode !== 200) return exit(`Failed to get app info from store: ${requestError(response)}`);
496
+ const [error, response] = await safe(superagent.get(url).ok(() => true));
497
+ if (error) throw new Error(`Failed to list apps from appstore: ${error.message}`);
498
+ if (response.statusCode !== 200) throw new Error(`Failed to get app info from store: ${requestError(response)}`);
544
499
 
545
- callback(null, response.body.manifest, null /* manifest file path */);
546
- });
500
+ return { manifest: response.body.manifest, manifestFilePath: null /* manifest file path */ };
547
501
  }
548
502
 
549
- function getManifest(appstoreId, callback) {
503
+ async function getManifest(appstoreId) {
550
504
  assert.strictEqual(typeof appstoreId, 'string');
551
- assert.strictEqual(typeof callback, 'function');
552
505
 
553
- if (appstoreId) return downloadManifest(appstoreId, callback);
506
+ if (appstoreId) return await downloadManifest(appstoreId);
554
507
 
555
- var manifestFilePath = helper.locateManifest();
556
- if (!manifestFilePath) return callback(new Error('No CloudronManifest.json found'));
508
+ const manifestFilePath = helper.locateManifest();
509
+ if (!manifestFilePath) throw new Error('No CloudronManifest.json found');
557
510
 
558
- var result = manifestFormat.parseFile(manifestFilePath);
559
- if (result.error) return exit('Invalid CloudronManifest.json: '.red + result.error.message);
511
+ const result = manifestFormat.parseFile(manifestFilePath);
512
+ if (result.error) throw new Error(`Invalid CloudronManifest.json: ${result.error.message}`);
560
513
 
561
514
  // resolve post install message
562
515
  if (result.manifest.postInstallMessage && result.manifest.postInstallMessage.slice(0, 7) === 'file://') {
563
- var postInstallFilename = result.manifest.postInstallMessage.slice(7);
516
+ let postInstallFilename = result.manifest.postInstallMessage.slice(7);
564
517
  // resolve filename wrt manifest
565
518
  if (manifestFilePath) postInstallFilename = path.resolve(path.dirname(manifestFilePath), postInstallFilename);
566
519
  result.manifest.postInstallMessage = safe.fs.readFileSync(postInstallFilename, 'utf8');
567
520
  if (!result.manifest.postInstallMessage) return exit('Could not read postInstallMessage file ' + postInstallFilename + ':' + safe.error.message);
568
521
  }
569
522
 
570
- callback(null, result.manifest, manifestFilePath);
523
+ return { manifest: result.manifest, manifestFilePath };
571
524
  }
572
525
 
573
- function install(options) {
526
+ async function install(options) {
574
527
  helper.verifyArguments(arguments);
575
528
 
576
- if (options.app) exit('Use "cloudron update" to update an app');
577
-
578
- getManifest(options.appstoreId || '', function (error, manifest, manifestFilePath) {
579
- if (error) return exit(error);
529
+ try {
530
+ const result = await getManifest(options.appstoreId || '');
531
+ const { manifest, manifestFilePath } = result;
580
532
 
581
533
  if (!manifest.dockerImage) {
582
534
  const sourceDir = path.dirname(manifestFilePath);
583
535
  const image = options.image || config.getAppConfig(sourceDir).dockerImage;
584
536
 
585
- if (!image) exit('No image found, please run `cloudron build` first or specify using --image');
537
+ if (!image) return exit('No image found, please run `cloudron build` first or specify using --image');
586
538
 
587
539
  manifest.dockerImage = image;
588
540
  }
@@ -590,340 +542,336 @@ function install(options) {
590
542
  const location = options.location || readlineSync.question('Location: ', {});
591
543
  if (!location) return exit('');
592
544
 
593
- selectDomain(location, options, function (error, domainObject) {
594
- if (error) return exit(error);
595
-
596
- // port bindings
597
- let portBindings = {};
598
- if (options.portBindings) {
599
- // ask the user for port values if the ports are different in the app and the manifest
600
- if (typeof options.portBindings === 'string') {
601
- portBindings = { };
602
- options.portBindings.split(',').forEach(function (kv) {
603
- var tmp = kv.split('=');
604
- if (isNaN(parseInt(tmp[1], 10))) return; // disable the port
605
- portBindings[tmp[0]] = parseInt(tmp[1], 10);
606
- });
607
- } else {
608
- portBindings = queryPortBindings(null /* existing app */, manifest);
609
- }
610
- } else { // just put in defaults
611
- const allPorts = _.extend({}, manifest.tcpPorts, manifest.udpPorts);
612
- for (let portName in allPorts) {
613
- portBindings[portName] = allPorts[portName].defaultValue;
545
+ const domainObject = await selectDomain(location, options);
546
+
547
+ // secondary domains
548
+ let secondaryDomains = {};
549
+ if (options.secondaryDomains) {
550
+ // ask the user for port values if the ports are different in the app and the manifest
551
+ if (typeof options.secondaryDomains === 'string') {
552
+ secondaryDomains = {};
553
+ for (const kv of options.secondaryDomains.split(',')) {
554
+ const tmp = kv.split('=');
555
+ secondaryDomains[tmp[0]] = await selectDomain(tmp[1], options);
614
556
  }
557
+ } else {
558
+ secondaryDomains = await querySecondaryDomains(null /* existing app */, manifest, options);
615
559
  }
616
-
617
- for (var binding in portBindings) {
618
- console.log('%s: %s', binding, portBindings[binding]);
560
+ } else if (manifest.httpPorts) { // just put in defaults
561
+ for (const env in manifest.httpPorts) {
562
+ secondaryDomains[env] = await selectDomain(manifest.httpPorts[env].defaultValue, options);
619
563
  }
564
+ }
620
565
 
621
- let data = {
622
- appStoreId: options.appstoreId || '', // note case change
623
- manifest: options.appstoreId ? null : manifest, // cloudron ignores manifest anyway if appStoreId is set
624
- location: domainObject.location,
625
- domain: domainObject.domain,
626
- portBindings: portBindings,
627
- accessRestriction: null
628
- };
629
-
630
- // the sso only applies for apps which allow optional sso
631
- if (manifest.optionalSso) data.sso = options.sso;
632
-
633
- if (options.debug) {
634
- data.debugMode = {
635
- readonlyRootfs: options.readonly ? true : false,
636
- cmd: parseDebugCommand(options.debug)
637
- };
638
- data.memoryLimit = -1;
639
- options.wait = false; // in debug mode, health check never succeeds
566
+ for (const binding in secondaryDomains) console.log(`Secondary domain ${binding}: ${secondaryDomains[binding].subdomain}.${secondaryDomains[binding].domain}`);
567
+
568
+ // port bindings
569
+ let portBindings = {};
570
+ if (options.portBindings) {
571
+ // ask the user for port values if the ports are different in the app and the manifest
572
+ if (typeof options.portBindings === 'string') {
573
+ portBindings = { };
574
+ options.portBindings.split(',').forEach(function (kv) {
575
+ let tmp = kv.split('=');
576
+ if (isNaN(parseInt(tmp[1], 10))) return; // disable the port
577
+ portBindings[tmp[0]] = parseInt(tmp[1], 10);
578
+ });
579
+ } else {
580
+ portBindings = queryPortBindings(null /* existing app */, manifest);
640
581
  }
641
-
642
- if (!options.appstoreId && manifest.icon) {
643
- let iconFilename = manifest.icon.slice(0, 7) === 'file://' ? manifest.icon.slice(7) : manifest.icon;
644
- iconFilename = path.resolve(path.dirname(manifestFilePath), iconFilename); // resolve filename wrt manifest
645
- data.icon = safe.fs.readFileSync(iconFilename, { encoding: 'base64' });
646
- if (!data.icon) return exit(`Could not read icon: ${iconFilename} message: ${safe.error.message}`);
582
+ } else { // just put in defaults
583
+ const allPorts = _.extend({}, manifest.tcpPorts, manifest.udpPorts);
584
+ for (let portName in allPorts) {
585
+ portBindings[portName] = allPorts[portName].defaultValue;
647
586
  }
587
+ }
648
588
 
649
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
589
+ for (const binding in portBindings) console.log(`Port ${binding}: ${portBindings[binding]}`);
590
+
591
+ const data = {
592
+ appStoreId: options.appstoreId || '', // note case change
593
+ manifest: options.appstoreId ? null : manifest, // cloudron ignores manifest anyway if appStoreId is set
594
+ location: domainObject.subdomain, // LEGACY
595
+ subdomain: domainObject.subdomain,
596
+ domain: domainObject.domain,
597
+ secondaryDomains,
598
+ portBindings,
599
+ accessRestriction: null
600
+ };
650
601
 
651
- request.post(`https://${adminFqdn}/api/v1/apps/install?access_token=${token}`, { rejectUnauthorized, json: data }, function (error, response) {
652
- if (error) return exit(error);
653
- if (response.statusCode !== 202) return exit(`Failed to install app: ${requestError(response)}`);
602
+ // the sso only applies for apps which allow optional sso
603
+ if (manifest.optionalSso) data.sso = options.sso;
654
604
 
655
- const appId = response.body.id;
605
+ if (options.debug) {
606
+ data.debugMode = {
607
+ readonlyRootfs: options.readonly ? true : false,
608
+ cmd: parseDebugCommand(options.debug)
609
+ };
610
+ data.memoryLimit = -1;
611
+ options.wait = false; // in debug mode, health check never succeeds
612
+ }
656
613
 
657
- console.log('App is being installed.');
614
+ if (!options.appstoreId && manifest.icon) {
615
+ let iconFilename = manifest.icon.slice(0, 7) === 'file://' ? manifest.icon.slice(7) : manifest.icon;
616
+ iconFilename = path.resolve(path.dirname(manifestFilePath), iconFilename); // resolve filename wrt manifest
617
+ data.icon = safe.fs.readFileSync(iconFilename, { encoding: 'base64' });
618
+ if (!data.icon) return exit(`Could not read icon: ${iconFilename} message: ${safe.error.message}`);
619
+ }
658
620
 
659
- waitForFinishInstallation(appId, response.body.taskId, options, function (error) {
660
- if (error) exit('\n\nApp installation error: %s'.red, error.message);
621
+ const request = createRequest('POST', '/api/v1/apps/install', options);
622
+ const response = await request.send(data);
623
+ if (response.statusCode !== 202) return exit(`Failed to install app: ${requestError(response)}`);
661
624
 
662
- console.log('\n\nApp is installed.'.green);
663
- });
664
- });
665
- });
666
- });
625
+ const appId = response.body.id;
626
+
627
+ console.log('App is being installed.');
628
+
629
+ await waitForFinishInstallation(appId, response.body.taskId, options);
630
+ console.log('\n\nApp is installed.');
631
+ } catch (error) {
632
+ exit('\n\nApp installation error: %s', error.message);
633
+ }
667
634
  }
668
635
 
669
- function configure(options) {
636
+ async function configure(options) {
670
637
  helper.verifyArguments(arguments);
671
638
 
672
- getApp(options, function (error, app) {
673
- if (error) return exit(error);
674
-
639
+ try {
640
+ const app = await getApp(options);
675
641
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
676
642
 
677
643
  if (!options.location) options.location = readlineSync.question(`Enter new location (default: ${app.fqdn}): `, { });
678
644
  let location = options.location || app.location;
679
645
 
680
- selectDomain(location, options, function (error, domainObject) {
681
- if (error) exit(error);
646
+ const domainObject = await selectDomain(location, options);
682
647
 
683
- const data = {
684
- location: domainObject.location,
685
- domain: domainObject.domain,
686
- portBindings: app.portBindings
648
+ const secondaryDomains = {};
649
+ app.secondaryDomains.forEach(sd => {
650
+ secondaryDomains[sd.environmentVariable] = {
651
+ subdomain: sd.subdomain,
652
+ domain: sd.domain
687
653
  };
654
+ });
688
655
 
689
- // port bindings
690
- if (options.portBindings) {
691
- var portBindings = app.portBindings;
692
- // ask the user for port values if the ports are different in the app and the manifest
693
- if (typeof options.portBindings === 'string') {
694
- portBindings = {};
695
- options.portBindings.split(',').forEach(function (kv) {
696
- var tmp = kv.split('=');
697
- if (isNaN(parseInt(tmp[1], 10))) return; // disable the port
698
- portBindings[tmp[0]] = parseInt(tmp[1], 10);
699
- });
700
- } else {
701
- portBindings = queryPortBindings(app, app.manifest);
702
- }
656
+ const data = {
657
+ location: domainObject.subdomain, // LEGACY
658
+ subdomain: domainObject.subdomain,
659
+ domain: domainObject.domain,
660
+ portBindings: app.portBindings,
661
+ secondaryDomains
662
+ };
703
663
 
704
- for (let binding in portBindings) {
705
- console.log('%s: %s', binding, portBindings[binding]);
664
+ // secondary domains
665
+ if (options.secondaryDomains) {
666
+ // ask the user for port values if the ports are different in the app and the manifest
667
+ if (typeof options.secondaryDomains === 'string') {
668
+ data.secondaryDomains = {};
669
+ for (const kv of options.secondaryDomains.split(',')) {
670
+ const tmp = kv.split('=');
671
+ data.secondaryDomains[tmp[0]] = await selectDomain(tmp[1], options);
706
672
  }
707
-
708
- data.portBindings = portBindings;
673
+ } else {
674
+ data.secondaryDomains = await querySecondaryDomains(app, app.manifest, options);
709
675
  }
710
676
 
711
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
677
+ for (const binding in data.secondaryDomains) console.log(`Secondary domain ${binding}: ${data.secondaryDomains[binding].subdomain}.${data.secondaryDomains[binding].domain}`);
678
+ }
712
679
 
713
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/configure/location?access_token=${token}`, { rejectUnauthorized, json: data }, function (error, response) {
714
- if (error) return exit(error);
715
- if (response.statusCode !== 202) return exit(`Failed to configure app: ${requestError(response)}`);
680
+ // port bindings
681
+ if (options.portBindings) {
682
+ let portBindings = app.portBindings;
683
+ // ask the user for port values if the ports are different in the app and the manifest
684
+ if (typeof options.portBindings === 'string') {
685
+ portBindings = {};
686
+ options.portBindings.split(',').forEach(function (kv) {
687
+ const tmp = kv.split('=');
688
+ if (isNaN(parseInt(tmp[1], 10))) return; // disable the port
689
+ portBindings[tmp[0]] = parseInt(tmp[1], 10);
690
+ });
691
+ } else {
692
+ portBindings = queryPortBindings(app, app.manifest);
693
+ }
716
694
 
717
- waitForTask(response.body.taskId, options, function (error) {
718
- if (error) return exit(error);
695
+ for (let binding in portBindings) {
696
+ console.log('%s: %s', binding, portBindings[binding]);
697
+ }
719
698
 
720
- waitForHealthy(app.id, options, function (error) {
721
- if (error) return exit(error);
699
+ data.portBindings = portBindings;
700
+ }
722
701
 
723
- console.log('\n\nApp configured'.green);
724
- });
725
- });
726
- });
727
- });
728
- });
702
+ const request = createRequest('POST', `/api/v1/apps/${app.id}/configure/location`, options);
703
+ const response = await request.send(data);
704
+ if (response.statusCode !== 202) return exit(`Failed to configure app: ${requestError(response)}`);
705
+
706
+ await waitForTask(response.body.taskId, options);
707
+ await waitForHealthy(app.id, options);
708
+ console.log('\n\nApp configured');
709
+ } catch (error) {
710
+ exit(error);
711
+ }
729
712
  }
730
713
 
731
- function update(options) {
714
+ async function update(options) {
732
715
  helper.verifyArguments(arguments);
733
716
 
734
- getApp(options, function (error, app) {
735
- if (error) return exit(error);
736
-
717
+ try {
718
+ const app = await getApp(options);
737
719
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
738
720
 
739
- getManifest(options.appstoreId || '', function (error, manifest, manifestFilePath) {
740
- if (error) return exit(error);
721
+ const result = await getManifest(options.appstoreId || '');
722
+ const { manifest, manifestFilePath } = result;
741
723
 
742
- let data = {};
724
+ let data = {};
743
725
 
744
- if (!manifest.dockerImage) {
745
- const sourceDir = path.dirname(manifestFilePath);
746
- const image = options.image || config.getAppConfig(sourceDir).dockerImage;
726
+ if (!manifest.dockerImage) {
727
+ const sourceDir = path.dirname(manifestFilePath);
728
+ const image = options.image || config.getAppConfig(sourceDir).dockerImage;
747
729
 
748
- if (!image) return exit('No image found, please run `cloudron build` first or use --image');
730
+ if (!image) return exit('No image found, please run `cloudron build` first or use --image');
749
731
 
750
- manifest.dockerImage = image;
732
+ manifest.dockerImage = image;
751
733
 
752
- if (manifest.icon) {
753
- let iconFilename = manifest.icon.slice(0, 7) === 'file://' ? manifest.icon.slice(7) : manifest.icon;
754
- iconFilename = path.resolve(path.dirname(manifestFilePath), iconFilename); // resolve filename wrt manifest
755
- data.icon = safe.fs.readFileSync(iconFilename, { encoding: 'base64' });
756
- if (!data.icon) return exit(`Could not read icon: ${iconFilename} message: ${safe.error.message}`);
757
- }
734
+ if (manifest.icon) {
735
+ let iconFilename = manifest.icon.slice(0, 7) === 'file://' ? manifest.icon.slice(7) : manifest.icon;
736
+ iconFilename = path.resolve(path.dirname(manifestFilePath), iconFilename); // resolve filename wrt manifest
737
+ data.icon = safe.fs.readFileSync(iconFilename, { encoding: 'base64' });
738
+ if (!data.icon) return exit(`Could not read icon: ${iconFilename} message: ${safe.error.message}`);
758
739
  }
740
+ }
759
741
 
760
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
761
- let url;
762
-
763
- if (app.error && (app.error.installationState === 'pending_install')) { // install had failed. call repair to re-install
764
- url = `https://${adminFqdn}/api/v1/apps/${app.id}/repair?access_token=${token}`;
765
- data = _.extend(data, { manifest });
766
- } else {
767
- url = `https://${adminFqdn}/api/v1/apps/${app.id}/update?access_token=${token}`;
768
- data = _.extend(data, {
769
- appStoreId: options.appstoreId || '', // note case change
770
- manifest: manifest,
771
- skipBackup: !options.backup,
772
- skipNotification: true,
773
- force: true // permit downgrades
774
- });
775
- }
742
+ let apiPath;
776
743
 
777
- request.post(url, { rejectUnauthorized, json: data }, function (error, response) {
778
- if (error) return exit(error);
779
- if (response.statusCode !== 202) return exit(`Failed to update app: ${requestError(response)}`);
744
+ if (app.error && (app.error.installationState === 'pending_install')) { // install had failed. call repair to re-install
745
+ apiPath = `/api/v1/apps/${app.id}/repair`;
746
+ data = _.extend(data, { manifest });
747
+ } else {
748
+ apiPath = `/api/v1/apps/${app.id}/update`;
749
+ data = _.extend(data, {
750
+ appStoreId: options.appstoreId || '', // note case change
751
+ manifest: manifest,
752
+ skipBackup: !options.backup,
753
+ skipNotification: true,
754
+ force: true // permit downgrades
755
+ });
756
+ }
780
757
 
781
- process.stdout.write('\n => ' + 'Waiting for app to be updated '.cyan);
758
+ const request = createRequest('POST', apiPath, options);
759
+ const response = await request.send(data);
760
+ if (response.statusCode !== 202) return exit(`Failed to update app: ${requestError(response)}`);
782
761
 
783
- waitForFinishInstallation(app.id, response.body.taskId, options, function (error) {
784
- if (error) exit('\n\nApp update error: %s'.red, error.message);
762
+ process.stdout.write('\n => ' + 'Waiting for app to be updated ');
785
763
 
786
- console.log('\n\nApp is updated.'.green);
787
- });
788
- });
789
- });
790
- });
764
+ await waitForFinishInstallation(app.id, response.body.taskId, options);
765
+ console.log('\n\nApp is updated.');
766
+ } catch (error) {
767
+ exit('\n\nApp update error: %s', error.message);
768
+ }
791
769
  }
792
770
 
793
- function debug(cmd, options) {
794
- getApp(options, function (error, app) {
795
- if (error) return exit(error);
796
-
771
+ async function debug(cmd, options) {
772
+ try {
773
+ const app = await getApp(options);
797
774
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
798
775
 
799
- var data = {
776
+ const data = {
800
777
  debugMode: options.disable ? null : {
801
778
  readonlyRootfs: options.readonly ? true : false,
802
779
  cmd: parseDebugCommand(cmd.join(' ').trim())
803
780
  }
804
781
  };
805
782
 
806
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
807
-
808
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/configure/debug_mode?access_token=${token}`, { rejectUnauthorized, json: data }, function (error, response) {
809
- if (error) return exit(error);
810
- if (response.statusCode !== 202) return exit(`Failed to set debug mode: ${requestError(response)}`);
783
+ const request = createRequest('POST', `/api/v1/apps/${app.id}/configure/debug_mode`, options);
784
+ const response = await request.send(data);
785
+ if (response.statusCode !== 202) return exit(`Failed to set debug mode: ${requestError(response)}`);
811
786
 
812
- waitForTask(response.body.taskId, options, function (error) {
813
- if (error) return exit(error);
787
+ await waitForTask(response.body.taskId, options);
814
788
 
815
- var memoryLimit = options.limitMemory ? 0 : -1;
789
+ const memoryLimit = options.limitMemory ? 0 : -1;
816
790
 
817
- // skip setting memory limit if unchanged
818
- if (app.memoryLimit === memoryLimit) return console.log('\n\nDone');
791
+ // skip setting memory limit if unchanged
792
+ if (app.memoryLimit === memoryLimit) return console.log('\n\nDone');
819
793
 
820
- console.log('\n');
821
- console.log(options.limitMemory ? 'Limiting memory' : 'Setting unlimited memory');
794
+ console.log('\n');
795
+ console.log(options.limitMemory ? 'Limiting memory' : 'Setting unlimited memory');
822
796
 
823
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/configure/memory_limit?access_token=${token}`, { rejectUnauthorized, json: { memoryLimit } }, function (error, response) {
824
- if (error) return exit(error);
825
- if (response.statusCode !== 202) return exit(`Failed to set memory limit: ${requestError(response)}`);
797
+ const request2 = createRequest('POST', `/api/v1/apps/${app.id}/configure/memory_limit`, options);
798
+ const response2 = await request2.send({ memoryLimit });
799
+ if (response2.statusCode !== 202) return exit(`Failed to set memory limit: ${requestError(response2)}`);
826
800
 
827
- waitForTask(response.body.taskId, options, function (error) {
828
- if (error) return exit(error);
829
-
830
- console.log('\n\nDone');
831
- });
832
- });
833
- });
834
- });
835
- });
801
+ await waitForTask(response2.body.taskId, options);
802
+ console.log('\n\nDone');
803
+ } catch (error) {
804
+ exit(error);
805
+ }
836
806
  }
837
807
 
838
- function repair(options) {
839
- getApp(options, function (error, app) {
840
- if (error) return exit(error);
841
-
808
+ async function repair(options) {
809
+ try {
810
+ const app = await getApp(options);
842
811
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
843
812
 
844
- getManifest('' /* appStoreId */, function (error, manifest, manifestFilePath) {
845
- let dockerImage = options.image;
846
- if (!dockerImage) {
847
- if (error) return exit(error);
848
-
849
- const sourceDir = path.dirname(manifestFilePath);
850
- dockerImage = config.getAppConfig(sourceDir).dockerImage;
851
- }
852
-
853
- if (!dockerImage) return exit('No image found, please run `cloudron build` first or use --image');
813
+ const { manifestFilePath } = await getManifest('' /* appStoreId */);
854
814
 
855
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
856
- const data = { dockerImage };
815
+ let dockerImage = options.image;
816
+ if (!dockerImage) {
817
+ const sourceDir = path.dirname(manifestFilePath);
818
+ dockerImage = config.getAppConfig(sourceDir).dockerImage;
819
+ }
857
820
 
858
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/repair?access_token=${token}`, { rejectUnauthorized, json: data }, function (error, response) {
859
- if (error) return exit(error);
860
- if (response.statusCode !== 202) return exit(`Failed to set repair mode: ${requestError(response)}`);
821
+ if (!dockerImage) return exit('No image found, please run `cloudron build` first or use --image');
861
822
 
862
- process.stdout.write('\n => ' + 'Waiting for app to be repaired '.cyan);
823
+ const request = createRequest('POST', `/api/v1/apps/${app.id}/repair`, options);
824
+ const response = await request.send({ dockerImage });
825
+ if (response.statusCode !== 202) return exit(`Failed to set repair mode: ${requestError(response)}`);
863
826
 
864
- waitForFinishInstallation(app.id, response.body.taskId, options, function (error) {
865
- if (error) exit('\n\nApp repair error: %s'.red, error.message);
827
+ process.stdout.write('\n => ' + 'Waiting for app to be repaired ');
866
828
 
867
- console.log('\n\nApp is repaired.'.green);
868
- });
869
- });
870
- });
871
- });
829
+ await waitForFinishInstallation(app.id, response.body.taskId, options);
830
+ console.log('\n\nApp is repaired.');
831
+ } catch (error) {
832
+ exit('App repair error: %s', error.message);
833
+ }
872
834
  }
873
835
 
874
- function cancel(options) {
836
+ async function cancel(options) {
875
837
  helper.verifyArguments(arguments);
876
838
 
877
- getApp(options, function (error, app) {
878
- if (error) return exit(error);
879
-
839
+ try {
840
+ const app = await getApp(options);
880
841
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
881
-
882
842
  if (!app.taskId) return exit('No active task.');
883
-
884
- stopActiveTask(app, options, function (error) {
885
- if (error) exit(error);
886
-
887
- console.log('\nTask stopped.'.green);
888
- });
889
- });
843
+ await stopActiveTask(app, options);
844
+ console.log('\nTask stopped.');
845
+ } catch (error) {
846
+ exit(error);
847
+ }
890
848
  }
891
849
 
892
- function uninstall(options) {
850
+ async function uninstall(options) {
893
851
  helper.verifyArguments(arguments);
894
852
 
895
- getApp(options, function (error, app) {
896
- if (error) return exit(error);
897
-
853
+ try {
854
+ const app = await getApp(options);
898
855
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
899
856
 
900
- stopActiveTask(app, options, function (error) {
901
- if (error) exit(error);
902
-
903
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
857
+ await stopActiveTask(app, options);
904
858
 
905
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/uninstall?access_token=${token}`, { rejectUnauthorized, json: true }, function (error, response) {
906
- if (error) return exit(error);
907
- if (response.statusCode !== 202) return exit(`Failed to uninstall app: ${requestError(response)}`);
859
+ const response = await createRequest('POST', `/api/v1/apps/${app.id}/uninstall`, options);
860
+ if (response.statusCode !== 202) return exit(`Failed to uninstall app: ${requestError(response)}`);
908
861
 
909
- process.stdout.write('\n => ' + 'Waiting for app to be uninstalled '.cyan);
862
+ process.stdout.write('\n => ' + 'Waiting for app to be uninstalled ');
910
863
 
911
- waitForTask(response.body.taskId, options, function (error) {
912
- if (error) return exit(error);
913
-
914
- request.get(`https://${adminFqdn}/api/v1/apps/${app.id}?access_token=${token}`, { rejectUnauthorized, json: true }, function (error, response) {
915
- if (error) return exit(error);
916
- if (response.statusCode === 404) {
917
- console.log('\n\nApp %s successfully uninstalled.', app.fqdn.bold);
918
- } else if (response.body.installationState === 'error') {
919
- console.log('\n\nApp uninstallation failed.\n'.red);
920
- exit(response.body.errorMessage);
921
- }
922
- });
923
- });
924
- });
925
- });
926
- });
864
+ await waitForTask(response.body.taskId, options);
865
+ const response2 = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
866
+ if (response2.statusCode === 404) {
867
+ console.log('\n\nApp %s successfully uninstalled.', app.fqdn);
868
+ } else if (response2.body.installationState === 'error') {
869
+ console.log('\n\nApp uninstallation failed.\n');
870
+ exit(response2.body.errorMessage);
871
+ }
872
+ } catch (error) {
873
+ exit(error);
874
+ }
927
875
  }
928
876
 
929
877
  function logPrinter(obj) {
@@ -933,308 +881,253 @@ function logPrinter(obj) {
933
881
  message = '[large binary blob skipped]';
934
882
  } else if (typeof obj.message === 'string') {
935
883
  message = obj.message;
936
- } else if (util.isArray(obj.message)) {
884
+ } else if (Array.isArray(obj.message)) {
937
885
  message = Buffer.from(obj.message).toString('utf8');
938
886
  }
939
887
 
940
- var ts = new Date(obj.realtimeTimestamp/1000).toTimeString().split(' ')[0];
941
- console.log('%s - %s', ts.cyan, message);
888
+ const ts = new Date(obj.realtimeTimestamp/1000).toTimeString().split(' ')[0];
889
+ console.log('%s - %s', ts, message);
942
890
  }
943
891
 
944
- function logs(options) {
892
+ async function logs(options) {
945
893
  helper.verifyArguments(arguments);
946
894
 
947
895
  const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
948
896
  const lines = options.lines || 500;
949
897
  const tail = !!options.tail;
950
-
951
- function processLogs(apiPath) {
952
- if (tail) {
953
- const url = `${apiPath}?access_token=${token}&lines=10`;
954
- var es = new EventSource(url, { rejectUnauthorized }); // not sure why this is needed
955
-
956
- es.on('message', function (e) { // e { type, data, lastEventId }. lastEventId is the timestamp
957
- logPrinter(JSON.parse(e.data));
958
- });
959
-
960
- es.on('error', function (error) {
961
- if (error.status === 401) return exit('Please login first');
962
- if (error.status === 412) exit('Logs currently not available.');
963
- exit(error);
964
- });
965
- } else {
966
- const url = `${apiPath}?access_token=${token}&lines=${lines}`;
967
-
968
- const req = request.get(url, { rejectUnauthorized }, function (error, response) {
969
- if (error) return exit(error);
970
- if (response.statusCode !== 200) return exit(`Failed to get logs: ${requestError(response)}`);
971
- });
972
-
973
- req.pipe(split(JSON.parse))
974
- .on('data', logPrinter)
975
- .on('error', process.exit)
976
- .on('end', process.exit);
977
- }
978
- }
898
+ let apiPath;
979
899
 
980
900
  if (typeof options.system === 'boolean' && options.system) {
981
901
  // box
982
- processLogs(`https://${adminFqdn}/api/v1/cloudron/${ tail ? 'logstream' : 'logs' }/box`);
902
+ apiPath = `https://${adminFqdn}/api/v1/cloudron/${ tail ? 'logstream' : 'logs' }/box`;
983
903
  } else if (typeof options.system === 'string') {
984
904
  // services
985
- processLogs(`https://${adminFqdn}/api/v1/services/${options.system}/${ tail ? 'logstream' : 'logs' }`);
905
+ apiPath = `https://${adminFqdn}/api/v1/services/${options.system}/${ tail ? 'logstream' : 'logs' }`;
986
906
  } else {
987
907
  // apps
908
+ const [error, app] = await safe(getApp(options));
909
+ if (error) return exit(error);
910
+ if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
911
+
912
+ apiPath = `https://${adminFqdn}/api/v1/apps/${app.id}/${ tail ? 'logstream' : 'logs' }`;
913
+ }
988
914
 
989
- getApp(options, function (error, app) {
990
- if (error) return exit(error);
991
- if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
915
+ if (tail) {
916
+ const url = `${apiPath}?access_token=${token}&lines=10`;
917
+ var es = new EventSource(url, { rejectUnauthorized }); // not sure why this is needed
992
918
 
993
- processLogs(`https://${adminFqdn}/api/v1/apps/${app.id}/${ tail ? 'logstream' : 'logs' }`);
919
+ es.on('message', function (e) { // e { type, data, lastEventId }. lastEventId is the timestamp
920
+ logPrinter(JSON.parse(e.data));
994
921
  });
922
+
923
+ es.on('error', function (error) {
924
+ if (error.status === 401) return exit('Please login first');
925
+ if (error.status === 412) exit('Logs currently not available.');
926
+ exit(error);
927
+ });
928
+ } else {
929
+ const url = `${apiPath}?access_token=${token}&lines=${lines}`;
930
+
931
+ const req = superagent.get(url, { rejectUnauthorized });
932
+ req.on('response', function (response) {
933
+ if (response.statusCode !== 200) return exit(`Failed to get logs: ${requestError(response)}`);
934
+ });
935
+ req.pipe(split(JSON.parse))
936
+ .on('data', logPrinter)
937
+ .on('error', process.exit)
938
+ .on('end', process.exit);
995
939
  }
996
940
  }
997
941
 
998
- function status(options) {
942
+ async function status(options) {
999
943
  helper.verifyArguments(arguments);
1000
944
 
1001
- getApp(options, function (error, app) {
1002
- if (error) return exit(error);
1003
-
1004
- if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
945
+ const [error, app] = await safe(getApp(options));
946
+ if (error) return exit(error);
947
+ if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1005
948
 
1006
- console.log();
1007
- console.log('Id: \t\t', app.id.cyan);
1008
- console.log('Location: \t', app.fqdn.cyan);
1009
- console.log('Version: \t', app.manifest.version.cyan);
1010
- console.log('Manifest Id: \t', (app.manifest.id || 'customapp').cyan);
1011
- console.log('Docker image: \t', (app.manifest.dockerImage).cyan);
1012
- console.log('Install state: \t', (app.installationState + (app.debugMode ? ' (debug)' : '')).cyan);
1013
- console.log('Run state: \t', app.runState.cyan);
1014
- console.log();
1015
-
1016
- exit();
1017
- });
949
+ console.log();
950
+ console.log('Id: \t\t', app.id);
951
+ console.log('Location: \t', app.fqdn);
952
+ console.log('Version: \t', app.manifest.version);
953
+ console.log('Manifest Id: \t', (app.manifest.id || 'customapp'));
954
+ console.log('Docker image: \t', (app.manifest.dockerImage));
955
+ console.log('Install state: \t', (app.installationState + (app.debugMode ? ' (debug)' : '')));
956
+ console.log('Run state: \t', app.runState);
957
+ console.log();
1018
958
  }
1019
959
 
1020
- function inspect(options) {
960
+ async function inspect(options) {
1021
961
  helper.verifyArguments(arguments);
1022
962
 
1023
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1024
-
1025
- request.get(`https://${adminFqdn}/api/v1/apps?access_token=${token}`, { rejectUnauthorized, json: true }, function (error, response) {
1026
- if (error) return exit(error);
963
+ try {
964
+ const response = await createRequest('GET', '/api/v1/apps', options);
1027
965
  if (response.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
1028
966
 
1029
967
  let apps = [];
1030
968
 
1031
- async.eachSeries(response.body.apps, function (app, iteratorDone) {
1032
- request.get(`https://${adminFqdn}/api/v1/apps/${app.id}?access_token=${token}`, { rejectUnauthorized, json: true }, function (error, response) {
1033
- if (error) return iteratorDone(error);
1034
- if (response.statusCode !== 200) return iteratorDone(`Failed to list app: ${requestError(response)}`);
969
+ for (const app of response.body.apps) {
970
+ const response2 = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
971
+ if (response2.statusCode !== 200) return exit(`Failed to list app: ${requestError(response2)}`);
972
+ response2.body.location = response2.body.subdomain; // LEGACY support
973
+ apps.push(response2.body);
974
+ }
1035
975
 
1036
- apps.push(response.body);
1037
- iteratorDone();
1038
- });
1039
- }, function (error) {
1040
- if (error) exit(error);
1041
-
1042
- console.log(JSON.stringify({
1043
- apiEndpoint: adminFqdn,
1044
- appStoreOrigin: config.appStoreOrigin(),
1045
- apps: apps
1046
- }, null, 4));
1047
- });
1048
- });
976
+ const { adminFqdn } = requestOptions(options);
977
+ console.log(JSON.stringify({
978
+ apiEndpoint: adminFqdn,
979
+ appStoreOrigin: config.appStoreOrigin(),
980
+ apps: apps
981
+ }, null, 4));
982
+ } catch (error) {
983
+ exit(error);
984
+ }
1049
985
  }
1050
986
 
1051
- function restart(options) {
987
+ async function restart(options) {
1052
988
  helper.verifyArguments(arguments);
1053
989
 
1054
- getApp(options, function (error, app) {
1055
- if (error) return exit(error);
1056
-
990
+ try {
991
+ const app = await getApp(options);
1057
992
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1058
-
1059
- stopApp(app, options, function (error) {
1060
- if (error) exit(error);
1061
-
1062
- startApp(app, options, function (error) {
1063
- if (error) return exit(error);
1064
-
1065
- waitForHealthy(app.id, options, function (error) {
1066
- if (error) return exit(error);
1067
-
1068
- console.log('\n\nApp restarted'.green);
1069
- });
1070
- });
1071
- });
1072
- });
993
+ await stopApp(app, options);
994
+ await startApp(app, options);
995
+ await waitForHealthy(app.id, options);
996
+ console.log('\n\nApp restarted');
997
+ } catch (error) {
998
+ exit(error);
999
+ }
1073
1000
  }
1074
1001
 
1075
- function start(options) {
1002
+ async function start(options) {
1076
1003
  helper.verifyArguments(arguments);
1077
1004
 
1078
- getApp(options, function (error, app) {
1079
- if (error) return exit(error);
1080
-
1005
+ try {
1006
+ const app = await getApp(options);
1081
1007
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1082
-
1083
- startApp(app, options, function (error) {
1084
- if (error) return exit(error);
1085
-
1086
- waitForHealthy(app.id, options, function (error) {
1087
- if (error) return exit(error);
1088
-
1089
- console.log('\n\nApp started'.green);
1090
- });
1091
- });
1092
- });
1008
+ await startApp(app, options);
1009
+ await waitForHealthy(app.id, options);
1010
+ console.log('\n\nApp started');
1011
+ } catch (error) {
1012
+ exit(error);
1013
+ }
1093
1014
  }
1094
1015
 
1095
- function stop(options) {
1016
+ async function stop(options) {
1096
1017
  helper.verifyArguments(arguments);
1097
1018
 
1098
- getApp(options, function (error, app) {
1099
- if (error) return exit(error);
1100
-
1019
+ try {
1020
+ const app = await getApp(options);
1101
1021
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1102
-
1103
- stopApp(app, options, function (error) {
1104
- if (error) exit(error);
1105
-
1106
- console.log('\n\nApp stopped'.green);
1107
-
1108
- exit();
1109
- });
1110
- });
1022
+ await stopApp(app, options);
1023
+ console.log('\n\nApp stopped');
1024
+ } catch (error) {
1025
+ exit(error);
1026
+ }
1111
1027
  }
1112
1028
 
1113
- function backupCreate(options) {
1029
+ async function backupCreate(options) {
1114
1030
  helper.verifyArguments(arguments);
1115
1031
 
1116
- getApp(options, function (error, app) {
1117
- if (error) return exit(error);
1118
-
1032
+ try {
1033
+ const app = await getApp(options);
1119
1034
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1120
1035
 
1121
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1122
-
1123
- // get existing options and then remove
1124
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/backup?access_token=${token}`, { json: {}, rejectUnauthorized }, function (error, response) {
1125
- if (error) return exit(error);
1126
- if (response.statusCode !== 202) return exit(`Failed to start backup: ${requestError(response)}`);
1036
+ const response = await createRequest('POST', `/api/v1/apps/${app.id}/backup`, options);
1037
+ if (response.statusCode !== 202) return exit(`Failed to start backup: ${requestError(response)}`);
1127
1038
 
1128
- // FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
1129
- waitForFinishBackup(app.id, response.body.taskId, options, function (error) {
1130
- if (error) return exit('\n\nApp backup error: %s'.red, error.message);
1131
-
1132
- console.log('\n\nApp is backed up'.green);
1133
- });
1134
- });
1135
- });
1039
+ // FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
1040
+ await waitForFinishBackup(app.id, response.body.taskId, options);
1041
+ console.log('\n\nApp is backed up');
1042
+ } catch (error) {
1043
+ exit('\n\nApp backup error: %s', error.message);
1044
+ }
1136
1045
  }
1137
1046
 
1138
- function backupList(options) {
1047
+ async function backupList(options) {
1139
1048
  helper.verifyArguments(arguments);
1140
1049
 
1141
- getApp(options, function (error, app) {
1142
- if (error) return exit(error);
1143
-
1050
+ try {
1051
+ const app = await getApp(options);
1144
1052
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1145
1053
 
1146
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1147
-
1148
- // get existing options and then remove
1149
- request.get(`https://${adminFqdn}/api/v1/apps/${app.id}/backups?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
1150
- if (error) return exit(error);
1151
- if (response.statusCode !== 200) return exit(`Failed to list backups: ${requestError(response)}`);
1054
+ const response = await createRequest('GET', `/api/v1/apps/${app.id}/backups`, options);
1055
+ if (response.statusCode !== 200) return exit(`Failed to list backups: ${requestError(response)}`);
1152
1056
 
1153
- if (options.raw) return console.log(JSON.stringify(response.body.backups, null, 4));
1057
+ if (options.raw) return console.log(JSON.stringify(response.body.backups, null, 4));
1154
1058
 
1155
- if (response.body.backups.length === 0) return console.log('No backups.');
1059
+ if (response.body.backups.length === 0) return console.log('No backups.');
1156
1060
 
1157
- response.body.backups = response.body.backups.map(function (backup) {
1158
- backup.creationTime = new Date(backup.creationTime);
1159
- return backup;
1160
- }).sort(function (a, b) { return b.creationTime - a.creationTime; });
1161
-
1162
- var t = new Table();
1061
+ response.body.backups = response.body.backups.map(function (backup) {
1062
+ backup.creationTime = new Date(backup.creationTime);
1063
+ return backup;
1064
+ }).sort(function (a, b) { return b.creationTime - a.creationTime; });
1163
1065
 
1164
- response.body.backups.forEach(function (backup) {
1165
- t.cell('Id', backup.id);
1166
- t.cell('Creation Time', backup.creationTime);
1167
- t.cell('Version', backup.packageVersion);
1066
+ var t = new Table();
1168
1067
 
1169
- t.newRow();
1170
- });
1068
+ response.body.backups.forEach(function (backup) {
1069
+ t.cell('Id', backup.id);
1070
+ t.cell('Creation Time', backup.creationTime);
1071
+ t.cell('Version', backup.packageVersion);
1171
1072
 
1172
- console.log();
1173
- console.log(t.toString());
1073
+ t.newRow();
1174
1074
  });
1175
- });
1176
- }
1177
1075
 
1178
- function getLastBackup(app, options, callback) {
1179
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1076
+ console.log();
1077
+ console.log(t.toString());
1078
+ } catch (error) {
1079
+ exit(error);
1080
+ }
1081
+ }
1180
1082
 
1181
- // get existing options and then remove
1182
- request.get(`https://${adminFqdn}/api/v1/apps/${app.id}/backups?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
1183
- if (error) return exit(error);
1184
- if (response.statusCode !== 200) return callback(new Error(`Failed to list backups: ${requestError(response)}`));
1185
- if (response.body.backups.length === 0) return callback(new Error('No backups'));
1083
+ async function getLastBackup(app, options) {
1084
+ const response = await createRequest('GET', `/api/v1/apps/${app.id}/backups`, options);
1085
+ if (response.statusCode !== 200) throw new Error(`Failed to list backups: ${requestError(response)}`);
1086
+ if (response.body.backups.length === 0) throw new Error('No backups');
1186
1087
 
1187
- response.body.backups = response.body.backups.map(function (backup) {
1188
- backup.creationTime = new Date(backup.creationTime);
1189
- return backup;
1190
- }).sort(function (a, b) { return b.creationTime - a.creationTime; });
1088
+ response.body.backups = response.body.backups.map(function (backup) {
1089
+ backup.creationTime = new Date(backup.creationTime);
1090
+ return backup;
1091
+ }).sort(function (a, b) { return b.creationTime - a.creationTime; });
1191
1092
 
1192
- return callback(null, response.body.backups[0].id);
1193
- });
1093
+ return response.body.backups[0].id;
1194
1094
  }
1195
1095
 
1196
- function restore(options) {
1096
+ async function restore(options) {
1197
1097
  helper.verifyArguments(arguments);
1198
1098
 
1199
- getApp(options, function (error, app) {
1200
- if (error) return exit(error);
1201
-
1099
+ try {
1100
+ const app = await getApp(options);
1202
1101
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1203
1102
 
1204
- var func = !options.backup ? getLastBackup.bind(null, app, options) : function (next) { next(null, options.backup); };
1205
-
1206
- func(function (error, backupId) {
1207
- if (error) return exit(error);
1103
+ let backupId;
1104
+ if (!options.backup) {
1105
+ backupId = await getLastBackup(app, options);
1106
+ } else {
1107
+ backupId = options.backup;
1108
+ }
1208
1109
 
1209
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1110
+ const request = createRequest('POST', `/api/v1/apps/${app.id}/restore`, options);
1111
+ const response = await request.send({ backupId });
1112
+ if (response.statusCode !== 202) return exit(`Failed to restore app: ${requestError(response)}`);
1210
1113
 
1211
- // get existing options and then remove
1212
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/restore?access_token=${token}`, { json: { backupId }, rejectUnauthorized }, function (error, response) {
1213
- if (error) return exit(error);
1214
- if (response.statusCode !== 202) return exit(`Failed to restore app: ${requestError(response)}`);
1114
+ // FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
1115
+ await waitForFinishInstallation(app.id, response.body.taskId, options);
1215
1116
 
1216
- // FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
1217
- waitForFinishInstallation(app.id, response.body.taskId, options, function (error) {
1218
- if (error) {
1219
- return exit('\n\nApp restore error: %s'.red, error.message);
1220
- }
1117
+ console.log('\n\nApp is restored');
1221
1118
 
1222
- console.log('\n\nApp is restored'.green);
1223
- });
1224
- });
1225
- });
1226
- });
1119
+ } catch (error) {
1120
+ exit('\n\nApp restore error: %s', error.message);
1121
+ }
1227
1122
  }
1228
1123
 
1229
- function importApp(options) {
1124
+ async function importApp(options) {
1230
1125
  helper.verifyArguments(arguments);
1231
1126
 
1232
- getApp(options, function (error, app) {
1233
- if (error) return exit(error);
1234
-
1127
+ try {
1128
+ const app = await getApp(options);
1235
1129
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1236
1130
 
1237
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1238
1131
  let data = {};
1239
1132
 
1240
1133
  if (!options.inPlace) {
@@ -1274,90 +1167,83 @@ function importApp(options) {
1274
1167
  if (backupKey) data.backupConfig.key = backupKey;
1275
1168
  }
1276
1169
 
1277
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/import?access_token=${token}`, { rejectUnauthorized, json: data }, function (error, response) {
1278
- if (error) return exit(error);
1279
- if (response.statusCode !== 202) return exit(`Failed to import app: ${requestError(response)}`);
1170
+ const response = await createRequest('POST', `/api/v1/apps/${app.id}/import`, options);
1171
+ if (response.statusCode !== 202) return exit(`Failed to import app: ${requestError(response)}`);
1280
1172
 
1281
- waitForFinishInstallation(app.id, response.body.taskId, options, function (error) {
1282
- if (error) return exit('\n\nApp import error: %s'.red, error.message);
1283
-
1284
- console.log('\n\nApp is restored'.green);
1285
- });
1286
- });
1287
- });
1173
+ await waitForFinishInstallation(app.id, response.body.taskId, options);
1174
+ console.log('\n\nApp is restored');
1175
+ } catch (error) {
1176
+ exit('\n\nApp import error: %s', error.message);
1177
+ }
1288
1178
  }
1289
1179
 
1290
- function exportApp(options) {
1180
+ async function exportApp(options) {
1291
1181
  helper.verifyArguments(arguments);
1292
1182
 
1293
- getApp(options, function (error, app) {
1294
- if (error) return exit(error);
1295
-
1183
+ try {
1184
+ const app = await getApp(options);
1296
1185
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1297
1186
 
1298
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1299
1187
  let data = {};
1300
1188
 
1301
1189
  if (!options.snapshot) return exit('--snapshot is required');
1302
1190
 
1303
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/export?access_token=${token}`, { rejectUnauthorized, json: data }, function (error, response) {
1304
- if (error) return exit(error);
1305
- if (response.statusCode !== 202) return exit(`Failed to export app: ${requestError(response)}`);
1191
+ const request = createRequest('POST', `/api/v1/apps/${app.id}/export`, options);
1192
+ const response = await request.send(data);
1193
+ if (response.statusCode !== 202) return exit(`Failed to export app: ${requestError(response)}`);
1306
1194
 
1307
- waitForFinishInstallation(app.id, response.body.taskId, options, function (error) {
1308
- if (error) return exit('\n\nApp export error: %s'.red, error.message);
1309
-
1310
- console.log('\n\nApp is exported'.green);
1311
- });
1312
- });
1313
- });
1195
+ await waitForFinishInstallation(app.id, response.body.taskId, options);
1196
+ console.log('\n\nApp is exported');
1197
+ } catch (error) {
1198
+ exit('\n\nApp export error: %s', error.message);
1199
+ }
1314
1200
  }
1315
1201
 
1316
- function clone(options) {
1202
+ async function clone(options) {
1317
1203
  helper.verifyArguments(arguments);
1318
1204
 
1319
- getApp(options, function (error, app) {
1320
- if (error) return exit(error);
1321
-
1205
+ try {
1206
+ const app = await getApp(options);
1322
1207
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1323
- if (!options.backup) return exit('Use --backup to specify the backup id');
1324
-
1325
- let location = options.location || readlineSync.question('Cloned app location: ', {});
1326
- let portBindings = queryPortBindings(app, app.manifest);
1327
- let backupId = options.backup;
1328
-
1329
- selectDomain(location, options, function (error, domainObject) {
1330
- if (error) return exit(error);
1331
-
1332
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1333
-
1334
- const data = { backupId, domain: domainObject.domain, location: domainObject.location, portBindings: portBindings };
1335
-
1336
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/clone?access_token=${token}`, { rejectUnauthorized, json: data }, function (error, response) {
1337
- if (error) return exit(error);
1338
- if (response.statusCode !== 201) return exit(`Failed to list apps: ${requestError(response)}`);
1339
1208
 
1340
- // FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
1341
- console.log('App cloned as id ' + response.body.id);
1342
- waitForFinishInstallation(response.body.id, response.body.taskId, options, function (error) {
1343
- if (error) return exit(`\n\nClone error: ${error.message}`);
1209
+ if (!options.backup) return exit('Use --backup to specify the backup id');
1344
1210
 
1345
- console.log('\n\nApp is cloned'.green);
1346
- });
1347
- });
1348
- });
1349
- });
1211
+ const location = options.location || readlineSync.question('Cloned app location: ', {});
1212
+ const secondaryDomains = await querySecondaryDomains(app, app.manifest, options);
1213
+ const portBindings = queryPortBindings(app, app.manifest);
1214
+ const backupId = options.backup;
1215
+
1216
+ const domainObject = await selectDomain(location, options);
1217
+ const data = {
1218
+ backupId,
1219
+ location: domainObject.subdomain, // LEGACY
1220
+ subdomain: domainObject.subdomain,
1221
+ domain: domainObject.domain,
1222
+ secondaryDomains,
1223
+ portBindings
1224
+ };
1225
+ const request = createRequest('POST', `/api/v1/apps/${app.id}/clone`, options);
1226
+ const response = await request.send(data);
1227
+ if (response.statusCode !== 201) return exit(`Failed to list apps: ${requestError(response)}`);
1228
+
1229
+ // FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
1230
+ console.log('App cloned as id ' + response.body.id);
1231
+ await waitForFinishInstallation(response.body.id, response.body.taskId, options);
1232
+ console.log('\n\nApp is cloned');
1233
+ } catch (error) {
1234
+ exit(`\n\nClone error: ${error.message}`);
1235
+ }
1350
1236
  }
1351
1237
 
1352
1238
  // taken from docker-modem
1353
1239
  function demuxStream(stream, stdout, stderr) {
1354
- var header = null;
1240
+ let header = null;
1355
1241
 
1356
1242
  stream.on('readable', function() {
1357
1243
  header = header || stream.read(8);
1358
1244
  while (header !== null) {
1359
- var type = header.readUInt8(0);
1360
- var payload = stream.read(header.readUInt32BE(4));
1245
+ const type = header.readUInt8(0);
1246
+ const payload = stream.read(header.readUInt32BE(4));
1361
1247
  if (payload === null) break;
1362
1248
  if (type == 2) {
1363
1249
  stderr.write(payload);
@@ -1375,96 +1261,94 @@ function demuxStream(stream, stdout, stderr) {
1375
1261
  // echo "sauce" | cloudron exec -- bash -c "cat - > /app/data/sauce" - test with binary files. should disable tty
1376
1262
  // cat ~/tmp/fantome.tar.gz | cloudron exec -- bash -c "tar xvf - -C /tmp" - must show an error
1377
1263
  // cat ~/tmp/fantome.tar.gz | cloudron exec -- bash -c "tar zxf - -C /tmp" - must extrack ok
1378
- function exec(cmd, options) {
1379
- var stdin = options._stdin || process.stdin; // hack for 'push', 'pull' to reuse this function
1380
- var stdout = options._stdout || process.stdout;
1264
+ async function exec(cmd, options) {
1265
+ let stdin = options._stdin || process.stdin; // hack for 'push', 'pull' to reuse this function
1266
+ let stdout = options._stdout || process.stdout;
1381
1267
 
1382
- var tty = !!options.tty;
1268
+ let tty = !!options.const;
1383
1269
 
1384
- getApp(options, function (error, app) {
1385
- if (error) return exit(error);
1270
+ const [error, app] = await safe(getApp(options));
1271
+ if (error) return exit(error);
1272
+ if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1386
1273
 
1387
- if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1388
-
1389
- if (app.installationState !== 'installed') exit('App is not yet running. Try again later.');
1390
-
1391
- if (cmd.length === 0) {
1392
- cmd = [ '/bin/bash' ];
1393
- tty = true; // override
1394
- }
1274
+ if (app.installationState !== 'installed') exit('App is not yet running. Try again later.');
1395
1275
 
1396
- if (tty && !stdin.isTTY) exit('stdin is not tty');
1276
+ if (cmd.length === 0) {
1277
+ cmd = [ '/bin/bash' ];
1278
+ tty = true; // override
1279
+ }
1397
1280
 
1398
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1281
+ if (tty && !stdin.isTTY) exit('stdin is not tty');
1399
1282
 
1400
- var query = {
1401
- rows: stdout.rows,
1402
- columns: stdout.columns,
1403
- access_token: token,
1404
- cmd: JSON.stringify(cmd),
1405
- tty: tty
1406
- };
1283
+ const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1407
1284
 
1408
- var req = https.request({
1409
- hostname: adminFqdn,
1410
- path: `/api/v1/apps/${app.id}/exec?` + querystring.stringify(query),
1411
- method: 'GET',
1412
- headers: {
1413
- 'Connection': 'Upgrade',
1414
- 'Upgrade': 'tcp'
1415
- },
1416
- rejectUnauthorized
1417
- }, function handler(res) {
1418
- if (res.statusCode === 403) exit('Unauthorized.'.red); // only admin or only owner (for docker addon)
1419
-
1420
- exit('Could not upgrade connection to tcp. http status:', res.statusCode);
1421
- });
1285
+ const query = {
1286
+ rows: stdout.rows,
1287
+ columns: stdout.columns,
1288
+ access_token: token,
1289
+ cmd: JSON.stringify(cmd),
1290
+ tty: tty
1291
+ };
1422
1292
 
1423
- req.on('upgrade', function (resThatShouldNotBeUsed, socket /*, upgradeHead*/) {
1424
- // do not use res here! it's all socket from here on
1425
- socket.on('error', exit);
1426
-
1427
- socket.setNoDelay(true);
1428
- socket.setKeepAlive(true);
1429
-
1430
- if (tty) {
1431
- stdin.setRawMode(true);
1432
- stdin.pipe(socket, { end: false }); // the remote will close the connection
1433
- socket.pipe(stdout); // in tty mode, stdout/stderr is merged
1434
- socket.on('end', exit); // server closed the socket
1435
- } else {// create stdin process on demand
1436
- if (typeof stdin === 'function') stdin = stdin();
1437
-
1438
- stdin.on('data', function (d) {
1439
- var buf = Buffer.alloc(4);
1440
- buf.writeUInt32BE(d.length, 0 /* offset */);
1441
- socket.write(buf);
1442
- socket.write(d);
1443
- });
1444
- stdin.on('end', function () {
1445
- var buf = Buffer.alloc(4);
1446
- buf.writeUInt32BE(0, 0 /* offset */);
1447
- socket.write(buf);
1448
- });
1293
+ const req = https.request({
1294
+ hostname: adminFqdn,
1295
+ path: `/api/v1/apps/${app.id}/exec?` + querystring.stringify(query),
1296
+ method: 'GET',
1297
+ headers: {
1298
+ 'Connection': 'Upgrade',
1299
+ 'Upgrade': 'tcp'
1300
+ },
1301
+ rejectUnauthorized
1302
+ }, function handler(res) {
1303
+ if (res.statusCode === 403) exit('Unauthorized.'); // only admin or only owner (for docker addon)
1304
+
1305
+ exit('Could not upgrade connection to tcp. http status:', res.statusCode);
1306
+ });
1449
1307
 
1450
- stdout.on('close', () => process.exit()); // this is only emitted when stdout is a file and not the terminal
1308
+ req.on('upgrade', function (resThatShouldNotBeUsed, socket /*, upgradeHead*/) {
1309
+ // do not use res here! it's all socket from here on
1310
+ socket.on('error', exit);
1311
+
1312
+ socket.setNoDelay(true);
1313
+ socket.setKeepAlive(true);
1314
+
1315
+ if (tty) {
1316
+ stdin.setRawMode(true);
1317
+ stdin.pipe(socket, { end: false }); // the remote will close the connection
1318
+ socket.pipe(stdout); // in tty mode, stdout/stderr is merged
1319
+ socket.on('end', exit); // server closed the socket
1320
+ } else {// create stdin process on demand
1321
+ if (typeof stdin === 'function') stdin = stdin();
1322
+
1323
+ stdin.on('data', function (d) {
1324
+ var buf = Buffer.alloc(4);
1325
+ buf.writeUInt32BE(d.length, 0 /* offset */);
1326
+ socket.write(buf);
1327
+ socket.write(d);
1328
+ });
1329
+ stdin.on('end', function () {
1330
+ var buf = Buffer.alloc(4);
1331
+ buf.writeUInt32BE(0, 0 /* offset */);
1332
+ socket.write(buf);
1333
+ });
1451
1334
 
1452
- demuxStream(socket, stdout, process.stderr); // can get separate streams in non-tty mode
1453
- socket.on('end', function () { // server closed the socket
1454
- stdin.end(); // required for this process to 'exit' cleanly. do not call exit() because writes may not have finished
1455
- if (stdout !== process.stdout) stdout.end(); // for push stream
1335
+ stdout.on('close', () => process.exit()); // this is only emitted when stdout is a file and not the terminal
1456
1336
 
1457
- socket.end();
1337
+ demuxStream(socket, stdout, process.stderr); // can get separate streams in non-tty mode
1338
+ socket.on('end', function () { // server closed the socket
1339
+ stdin.end(); // required for this process to 'exit' cleanly. do not call exit() because writes may not have finished
1340
+ if (stdout !== process.stdout) stdout.end(); // for push stream
1458
1341
 
1459
- // process._getActiveHandles(); process._getActiveRequests();
1460
- if (stdout === process.stdout) setImmediate(() => process.exit()); // otherwise, we rely on the 'close' event above
1461
- });
1462
- }
1463
- });
1342
+ socket.end();
1464
1343
 
1465
- req.on('error', exit); // could not make a request
1466
- req.end(); // this makes the request
1344
+ // process._getActiveHandles(); process._getActiveRequests();
1345
+ if (stdout === process.stdout) setImmediate(() => process.exit()); // otherwise, we rely on the 'close' event above
1346
+ });
1347
+ }
1467
1348
  });
1349
+
1350
+ req.on('error', exit); // could not make a request
1351
+ req.end(); // this makes the request
1468
1352
  }
1469
1353
 
1470
1354
  function push(localDir, remote, options) {
@@ -1537,184 +1421,132 @@ function pull(remote, local, options) {
1537
1421
  }
1538
1422
  }
1539
1423
 
1540
- function createOAuthAppCredentials(options) {
1541
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1542
-
1543
- const data = { appId: 'localdevelopment', redirectURI: options.redirectUri, scope: options.scope };
1544
-
1545
- request.post(`https://${adminFqdn}/api/v1/clients?access_token=${token}`, { json: data, rejectUnauthorized }, function (error, response) {
1546
- if (error) return exit(error);
1547
- if (response.statusCode !== 201) return exit(`Failed to create oauth app credentials: ${requestError(response)}`);
1548
-
1549
- if (options.shell) {
1550
- console.log('export CLOUDRON_CLIENT_ID="%s";\nexport CLOUDRON_CLIENT_SECRET="%s";\nexport CLOUDRON_REDIRECT_URI="%s"\nexport CLOUDRON_API_ORIGIN="%s"',
1551
- response.body.id, response.body.clientSecret, response.body.redirectURI, `https://${adminFqdn}`);
1552
- } else {
1553
- console.log();
1554
- console.log('Useful OAuth information:'.bold);
1555
- console.log(`apiOrigin: https://${adminFqdn}`);
1556
- console.log(`authorizationURL: https://${adminFqdn}/api/v1/oauth/dialog/authorize`);
1557
- console.log(`tokenURL: https://${adminFqdn}/api/v1/oauth/token`);
1558
- console.log();
1559
- console.log('New oauth app credentials:'.bold);
1560
- console.log('ClientId: %s', response.body.id.cyan);
1561
- console.log('ClientSecret: %s', response.body.clientSecret.cyan);
1562
- console.log('RedirectURI: %s', response.body.redirectURI.cyan);
1563
- console.log();
1564
- console.log('Gulp development:'.bold);
1565
- console.log(`gulp develop --api-origin https://${adminFqdn} --client-id ${response.body.id} --client-secret ${response.body.clientSecret}`);
1566
- }
1567
- });
1568
- }
1569
-
1570
1424
  function init() {
1571
- var manifestFilePath = helper.locateManifest();
1572
- if (manifestFilePath && path.dirname(manifestFilePath) === process.cwd()) return exit('CloudronManifest.json already exists in current directory'.red);
1425
+ const manifestFilePath = helper.locateManifest();
1426
+ if (manifestFilePath && path.dirname(manifestFilePath) === process.cwd()) return exit('CloudronManifest.json already exists in current directory');
1573
1427
 
1574
- var manifestTemplate = fs.readFileSync(path.join(__dirname, 'templates/', 'CloudronManifest.json.ejs'), 'utf8');
1575
- var dockerfileTemplate = fs.readFileSync(path.join(__dirname, 'templates/', 'Dockerfile.ejs'), 'utf8');
1576
- var dockerignoreTemplate = fs.readFileSync(path.join(__dirname, 'templates/', 'dockerignore.ejs'), 'utf8');
1428
+ const manifestTemplate = fs.readFileSync(path.join(__dirname, 'templates/', 'CloudronManifest.json.ejs'), 'utf8');
1429
+ const dockerfileTemplate = fs.readFileSync(path.join(__dirname, 'templates/', 'Dockerfile.ejs'), 'utf8');
1430
+ const dockerignoreTemplate = fs.readFileSync(path.join(__dirname, 'templates/', 'dockerignore.ejs'), 'utf8');
1577
1431
 
1578
- var data = {
1432
+ const data = {
1579
1433
  version: '0.1.0',
1580
1434
  title: 'App title',
1581
1435
  httpPort: 3000
1582
1436
  };
1583
1437
 
1584
- var manifest = ejs.render(manifestTemplate, data);
1438
+ const manifest = ejs.render(manifestTemplate, data);
1585
1439
  fs.writeFileSync('CloudronManifest.json', manifest, 'utf8');
1586
1440
 
1587
1441
  if (fs.existsSync('Dockerfile')) {
1588
1442
  console.log('Dockerfile already exists, skipping');
1589
1443
  } else {
1590
- var dockerfile = ejs.render(dockerfileTemplate, data);
1444
+ const dockerfile = ejs.render(dockerfileTemplate, data);
1591
1445
  fs.writeFileSync('Dockerfile', dockerfile, 'utf8');
1592
1446
  }
1593
1447
 
1594
1448
  if (fs.existsSync('.dockerignore')) {
1595
1449
  console.log('.dockerignore already exists, skipping');
1596
1450
  } else {
1597
- var dockerignore = ejs.render(dockerignoreTemplate, data);
1451
+ const dockerignore = ejs.render(dockerignoreTemplate, data);
1598
1452
  fs.writeFileSync('.dockerignore', dockerignore, 'utf8');
1599
1453
  }
1600
1454
 
1601
1455
  console.log();
1602
- console.log('Now edit the CloudronManifest.json'.yellow.bold);
1456
+ console.log('Now edit the CloudronManifest.json');
1603
1457
  console.log();
1604
1458
  }
1605
1459
 
1606
- function setEnv(app, env, options, callback) {
1460
+ async function setEnv(app, env, options) {
1607
1461
  assert.strictEqual(typeof app, 'object');
1608
1462
  assert.strictEqual(typeof env, 'object');
1609
1463
  assert.strictEqual(typeof options, 'object');
1610
- assert.strictEqual(typeof callback, 'function');
1611
1464
 
1612
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1613
-
1614
- // get existing options first and then merge
1615
- request.post(`https://${adminFqdn}/api/v1/apps/${app.id}/configure/env?access_token=${token}`, { json: { env }, rejectUnauthorized }, function (error, response) {
1616
- if (error) return exit(error);
1617
- if (response.statusCode !== 202) return exit(`Failed to set env: ${requestError(response)}`);
1618
-
1619
- waitForTask(response.body.taskId, options, function (error) {
1620
- if (error) return callback(error);
1465
+ const request = createRequest('POST', `/api/v1/apps/${app.id}/configure/env`, options);
1466
+ const response = await request.send({ env });
1467
+ if (response.statusCode !== 202) return exit(`Failed to set env: ${requestError(response)}`);
1621
1468
 
1622
- console.log('\n');
1623
- });
1624
- });
1469
+ await waitForTask(response.body.taskId, options);
1470
+ console.log('\n');
1625
1471
  }
1626
1472
 
1627
- function envSet(envVars, options) {
1628
- getApp(options, function (error, app) {
1629
- if (error) return exit(error);
1630
-
1473
+ async function envSet(envVars, options) {
1474
+ try {
1475
+ const app = await getApp(options);
1631
1476
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1632
1477
 
1633
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1634
-
1635
- // get existing options first and then merge
1636
- request.get(`https://${adminFqdn}/api/v1/apps/${app.id}?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
1637
- if (error) return exit(error);
1638
- if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1639
-
1640
- let env = response.body.env;
1641
- envVars.forEach(envVar => {
1642
- let m = envVar.match(/(.*?)=(.*)/);
1643
- if (!m) return exit(`Expecting KEY=VALUE pattern. Got ${envVar}`);
1644
- env[m[1]] = m[2];
1645
- });
1478
+ const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1479
+ if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1646
1480
 
1647
- setEnv(app, env, options, exit);
1481
+ let env = response.body.env;
1482
+ envVars.forEach(envVar => {
1483
+ let m = envVar.match(/(.*?)=(.*)/);
1484
+ if (!m) return exit(`Expecting KEY=VALUE pattern. Got ${envVar}`);
1485
+ env[m[1]] = m[2];
1648
1486
  });
1649
- });
1650
- }
1651
1487
 
1652
- function envUnset(envNames, options) {
1653
- getApp(options, function (error, app) {
1654
- if (error) return exit(error);
1488
+ await setEnv(app, env, options);
1489
+ } catch (error) {
1490
+ exit(error);
1491
+ }
1492
+ }
1655
1493
 
1494
+ async function envUnset(envNames, options) {
1495
+ try {
1496
+ const app = await getApp(options);
1656
1497
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1657
1498
 
1658
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1659
-
1660
- // get existing options and then remove
1661
- request.get(`https://${adminFqdn}/api/v1/apps/${app.id}?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
1662
- if (error) return exit(error);
1663
- if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1499
+ const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1500
+ if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1664
1501
 
1665
- let env = response.body.env;
1666
- envNames.forEach(name => delete env[name]);
1502
+ let env = response.body.env;
1503
+ envNames.forEach(name => delete env[name]);
1667
1504
 
1668
- setEnv(app, env, options, exit);
1669
- });
1670
- });
1505
+ await setEnv(app, env, options);
1506
+ } catch (error) {
1507
+ exit(error);
1508
+ }
1671
1509
  }
1672
1510
 
1673
- function envList(options) {
1511
+ async function envList(options) {
1674
1512
  helper.verifyArguments(arguments);
1675
1513
 
1676
- getApp(options, function (error, app) {
1677
- if (error) return exit(error);
1678
-
1514
+ try {
1515
+ const app = await getApp(options);
1679
1516
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1680
1517
 
1681
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1518
+ const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1519
+ if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1682
1520
 
1683
- request.get(`https://${adminFqdn}/api/v1/apps/${app.id}?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
1684
- if (error) return exit(error);
1685
- if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1521
+ const t = new Table();
1686
1522
 
1687
- var t = new Table();
1523
+ const envNames = Object.keys(response.body.env);
1524
+ if (envNames.length === 0) return console.log('No custom environment variables');
1688
1525
 
1689
- const envNames = Object.keys(response.body.env);
1690
- if (envNames.length === 0) return console.log('No custom environment variables');
1526
+ envNames.forEach(function (envName) {
1527
+ t.cell('Name', envName);
1528
+ t.cell('Value', response.body.env[envName]);
1691
1529
 
1692
- envNames.forEach(function (envName) {
1693
- t.cell('Name', envName);
1694
- t.cell('Value', response.body.env[envName]);
1695
-
1696
- t.newRow();
1697
- });
1698
-
1699
- console.log();
1700
- console.log(t.toString());
1530
+ t.newRow();
1701
1531
  });
1702
- });
1703
- }
1704
1532
 
1705
- function envGet(envName, options) {
1706
- getApp(options, function (error, app) {
1707
- if (error) return exit(error);
1533
+ console.log();
1534
+ console.log(t.toString());
1535
+ } catch (error) {
1536
+ exit(error);
1537
+ }
1538
+ }
1708
1539
 
1540
+ async function envGet(envName, options) {
1541
+ try {
1542
+ const app = await getApp(options);
1709
1543
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1710
1544
 
1711
- const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
1712
-
1713
- request.get(`https://${adminFqdn}/api/v1/apps/${app.id}?access_token=${token}`, { json: true, rejectUnauthorized }, function (error, response) {
1714
- if (error) return exit(error);
1715
- if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1545
+ const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1546
+ if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1716
1547
 
1717
- console.log(response.body.env[envName]);
1718
- });
1719
- });
1548
+ console.log(response.body.env[envName]);
1549
+ } catch (error) {
1550
+ exit(error);
1551
+ }
1720
1552
  }