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