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/bin/cloudron +24 -29
- package/bin/cloudron-appstore +0 -1
- package/bin/cloudron-backup +0 -1
- package/bin/cloudron-env +0 -1
- package/package.json +11 -14
- package/src/actions.js +766 -1005
- package/src/appstore-actions.js +38 -40
- package/src/build-actions.js +22 -22
- package/src/completion.js +0 -2
- package/src/helper.js +2 -2
- package/src/templates/Dockerfile.ejs +1 -1
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
|
|
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
|
|
101
|
+
const { adminFqdn } = requestOptions(options);
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
106
|
+
const domains = response.body.domains;
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
});
|
|
124
|
+
return { location, domain };
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
function stopActiveTask(app, options
|
|
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
|
|
131
|
+
if (!app.taskId) return;
|
|
129
132
|
|
|
130
133
|
console.log(`Stopping app's current active task ${app.taskId}`);
|
|
131
134
|
|
|
132
|
-
const
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
150
|
+
if (matchingApps.length === 0) return [ ];
|
|
151
|
+
if (matchingApps.length === 1) return matchingApps[0];
|
|
159
152
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
});
|
|
168
|
+
return matchingApps[index];
|
|
177
169
|
}
|
|
178
170
|
|
|
179
171
|
// appId may be the appId or the location
|
|
180
|
-
function getApp(options
|
|
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)
|
|
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)
|
|
185
|
+
if (!appConfig.repository) throw new Error(NO_APP_FOUND_ERROR_STRING);
|
|
196
186
|
|
|
197
|
-
selectAppWithRepository(appConfig.repository, options
|
|
198
|
-
|
|
187
|
+
const [error, result] = await safe(selectAppWithRepository(appConfig.repository, options));
|
|
188
|
+
if (error || result.length === 0) return null;
|
|
199
189
|
|
|
200
|
-
|
|
201
|
-
});
|
|
190
|
+
return result;
|
|
202
191
|
} else if (app.match(/.{8}-.{4}-.{4}-.{4}-.{8}/)) { // it is an id
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
});
|
|
195
|
+
return response.body;
|
|
209
196
|
} else { // it is a location
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
});
|
|
203
|
+
return match[0];
|
|
219
204
|
}
|
|
220
205
|
}
|
|
221
206
|
|
|
222
|
-
function
|
|
223
|
-
assert.strictEqual(typeof
|
|
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
|
-
|
|
211
|
+
if (!options.wait) return;
|
|
228
212
|
|
|
229
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
if (response.statusCode !== 202) return callback(`Failed to stop app: ${requestError(response)}`);
|
|
310
|
+
await waitForTask(response.body.taskId, options);
|
|
311
|
+
}
|
|
247
312
|
|
|
248
|
-
|
|
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(
|
|
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'
|
|
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(
|
|
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.'
|
|
373
|
+
console.log('Existing token still valid.');
|
|
303
374
|
} else {
|
|
304
375
|
token = null;
|
|
305
|
-
console.log(`Existing token possibly expired: ${requestError(response)}
|
|
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.'
|
|
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
|
-
|
|
337
|
-
|
|
406
|
+
async function open(options) {
|
|
407
|
+
const [error, app] = await safe(getApp(options));
|
|
408
|
+
if (error) return exit(error);
|
|
338
409
|
|
|
339
|
-
|
|
410
|
+
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
340
411
|
|
|
341
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
546
|
-
});
|
|
486
|
+
return { manifest: response.body.manifest, manifestFilePath: null /* manifest file path */ };
|
|
547
487
|
}
|
|
548
488
|
|
|
549
|
-
function getManifest(appstoreId
|
|
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
|
|
492
|
+
if (appstoreId) return await downloadManifest(appstoreId);
|
|
554
493
|
|
|
555
|
-
|
|
556
|
-
if (!manifestFilePath)
|
|
494
|
+
const manifestFilePath = helper.locateManifest();
|
|
495
|
+
if (!manifestFilePath) throw new Error('No CloudronManifest.json found');
|
|
557
496
|
|
|
558
|
-
|
|
559
|
-
if (result.error)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
618
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
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
|
-
|
|
590
|
+
const appId = response.body.id;
|
|
658
591
|
|
|
659
|
-
|
|
660
|
-
if (error) exit('\n\nApp installation error: %s'.red, error.message);
|
|
592
|
+
console.log('App is being installed.');
|
|
661
593
|
|
|
662
|
-
|
|
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
|
-
|
|
673
|
-
|
|
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
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
613
|
+
const data = {
|
|
614
|
+
location: domainObject.location,
|
|
615
|
+
domain: domainObject.domain,
|
|
616
|
+
portBindings: app.portBindings
|
|
617
|
+
};
|
|
707
618
|
|
|
708
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
718
|
-
|
|
638
|
+
data.portBindings = portBindings;
|
|
639
|
+
}
|
|
719
640
|
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
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
|
-
|
|
735
|
-
|
|
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 || ''
|
|
740
|
-
|
|
660
|
+
const result = await getManifest(options.appstoreId || '');
|
|
661
|
+
const { manifest, manifestFilePath } = result;
|
|
741
662
|
|
|
742
|
-
|
|
663
|
+
let data = {};
|
|
743
664
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
665
|
+
if (!manifest.dockerImage) {
|
|
666
|
+
const sourceDir = path.dirname(manifestFilePath);
|
|
667
|
+
const image = options.image || config.getAppConfig(sourceDir).dockerImage;
|
|
747
668
|
|
|
748
|
-
|
|
669
|
+
if (!image) return exit('No image found, please run `cloudron build` first or use --image');
|
|
749
670
|
|
|
750
|
-
|
|
671
|
+
manifest.dockerImage = image;
|
|
751
672
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
795
|
-
|
|
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
|
-
|
|
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
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
813
|
-
if (error) return exit(error);
|
|
726
|
+
await waitForTask(response.body.taskId, options);
|
|
814
727
|
|
|
815
|
-
|
|
728
|
+
const memoryLimit = options.limitMemory ? 0 : -1;
|
|
816
729
|
|
|
817
|
-
|
|
818
|
-
|
|
730
|
+
// skip setting memory limit if unchanged
|
|
731
|
+
if (app.memoryLimit === memoryLimit) return console.log('\n\nDone');
|
|
819
732
|
|
|
820
|
-
|
|
821
|
-
|
|
733
|
+
console.log('\n');
|
|
734
|
+
console.log(options.limitMemory ? 'Limiting memory' : 'Setting unlimited memory');
|
|
822
735
|
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
840
|
-
|
|
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
|
|
845
|
-
let dockerImage = options.image;
|
|
846
|
-
if (!dockerImage) {
|
|
847
|
-
if (error) return exit(error);
|
|
751
|
+
const { manifestFilePath } = await getManifest('' /* appStoreId */);
|
|
848
752
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
878
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
896
|
-
|
|
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
|
|
901
|
-
if (error) exit(error);
|
|
902
|
-
|
|
903
|
-
const { adminFqdn, token, rejectUnauthorized } = requestOptions(options);
|
|
795
|
+
await stopActiveTask(app, options);
|
|
904
796
|
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
800
|
+
process.stdout.write('\n => ' + 'Waiting for app to be uninstalled ');
|
|
910
801
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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 (
|
|
822
|
+
} else if (Array.isArray(obj.message)) {
|
|
937
823
|
message = Buffer.from(obj.message).toString('utf8');
|
|
938
824
|
}
|
|
939
825
|
|
|
940
|
-
|
|
941
|
-
console.log('%s - %s', ts
|
|
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
|
-
|
|
840
|
+
apiPath = `https://${adminFqdn}/api/v1/cloudron/${ tail ? 'logstream' : 'logs' }/box`;
|
|
983
841
|
} else if (typeof options.system === 'string') {
|
|
984
842
|
// services
|
|
985
|
-
|
|
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
|
-
|
|
990
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
927
|
+
try {
|
|
928
|
+
const app = await getApp(options);
|
|
1057
929
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
942
|
+
try {
|
|
943
|
+
const app = await getApp(options);
|
|
1081
944
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
956
|
+
try {
|
|
957
|
+
const app = await getApp(options);
|
|
1101
958
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
969
|
+
try {
|
|
970
|
+
const app = await getApp(options);
|
|
1119
971
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1120
972
|
|
|
1121
|
-
const
|
|
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
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
987
|
+
try {
|
|
988
|
+
const app = await getApp(options);
|
|
1144
989
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1145
990
|
|
|
1146
|
-
const
|
|
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
|
-
|
|
994
|
+
if (options.raw) return console.log(JSON.stringify(response.body.backups, null, 4));
|
|
1154
995
|
|
|
1155
|
-
|
|
996
|
+
if (response.body.backups.length === 0) return console.log('No backups.');
|
|
1156
997
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1173
|
-
console.log(t.toString());
|
|
1010
|
+
t.newRow();
|
|
1174
1011
|
});
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
1012
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1013
|
+
console.log();
|
|
1014
|
+
console.log(t.toString());
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
exit(error);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1180
1019
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1036
|
+
try {
|
|
1037
|
+
const app = await getApp(options);
|
|
1202
1038
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1203
1039
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1040
|
+
let backupId;
|
|
1041
|
+
if (!options.backup) {
|
|
1042
|
+
backupId = await getLastBackup(app, options);
|
|
1043
|
+
} else {
|
|
1044
|
+
backupId = options.backup;
|
|
1045
|
+
}
|
|
1208
1046
|
|
|
1209
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
1278
|
-
|
|
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
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
|
|
1294
|
-
|
|
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
|
|
1304
|
-
|
|
1305
|
-
|
|
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
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
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
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
1197
|
+
let tty = !!options.const;
|
|
1383
1198
|
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1205
|
+
if (cmd.length === 0) {
|
|
1206
|
+
cmd = [ '/bin/bash' ];
|
|
1207
|
+
tty = true; // override
|
|
1208
|
+
}
|
|
1397
1209
|
|
|
1398
|
-
|
|
1210
|
+
if (tty && !stdin.isTTY) exit('stdin is not tty');
|
|
1399
1211
|
|
|
1400
|
-
|
|
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
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1466
|
-
|
|
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
|
-
|
|
1572
|
-
if (manifestFilePath && path.dirname(manifestFilePath) === process.cwd()) return exit('CloudronManifest.json already exists in current directory'
|
|
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
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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
|
-
|
|
1361
|
+
const data = {
|
|
1579
1362
|
version: '0.1.0',
|
|
1580
1363
|
title: 'App title',
|
|
1581
1364
|
httpPort: 3000
|
|
1582
1365
|
};
|
|
1583
1366
|
|
|
1584
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
1385
|
+
console.log('Now edit the CloudronManifest.json');
|
|
1603
1386
|
console.log();
|
|
1604
1387
|
}
|
|
1605
1388
|
|
|
1606
|
-
function setEnv(app, env, options
|
|
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
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
-
|
|
1623
|
-
|
|
1624
|
-
});
|
|
1398
|
+
await waitForTask(response.body.taskId, options);
|
|
1399
|
+
console.log('\n');
|
|
1625
1400
|
}
|
|
1626
1401
|
|
|
1627
|
-
function envSet(envVars, options) {
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
|
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
|
-
|
|
1666
|
-
|
|
1431
|
+
let env = response.body.env;
|
|
1432
|
+
envNames.forEach(name => delete env[name]);
|
|
1667
1433
|
|
|
1668
|
-
|
|
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
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1443
|
+
try {
|
|
1444
|
+
const app = await getApp(options);
|
|
1679
1445
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1680
1446
|
|
|
1681
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1452
|
+
const envNames = Object.keys(response.body.env);
|
|
1453
|
+
if (envNames.length === 0) return console.log('No custom environment variables');
|
|
1688
1454
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1455
|
+
envNames.forEach(function (envName) {
|
|
1456
|
+
t.cell('Name', envName);
|
|
1457
|
+
t.cell('Value', response.body.env[envName]);
|
|
1691
1458
|
|
|
1692
|
-
|
|
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
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1477
|
+
console.log(response.body.env[envName]);
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
exit(error);
|
|
1480
|
+
}
|
|
1720
1481
|
}
|