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