cloudron 5.14.10 → 5.16.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/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+
2
+ [5.14.0]
3
+ * Replace superagent module with own implementation.
4
+
5
+ [5.14.1]
6
+ * fix usage of request.attach
7
+
8
+ [5.14.2]
9
+ * superagent does not return stream anymore
10
+
11
+ [5.14.3]
12
+ * Handle case where info subcommand reveals no published versions
13
+ * Add better error message if a URL with protocol was set as build repository
14
+
15
+ [5.14.4]
16
+ * fixes crash in safetydance when execSync returns null
17
+
18
+ [5.14.5]
19
+ * case insensitive compare
20
+
21
+ [5.14.6]
22
+ * Fix typo in appstore notifications
23
+
24
+ [5.14.7]
25
+ * Update base image in template
26
+
27
+ [5.14.8]
28
+ * Always send a request body on POST
29
+ * Ensure we send an object on uninstall to enforce the expected content-type
30
+
31
+ [5.14.9]
32
+ * check image in docker registry instead of docker hub web page since docker hub is down
33
+
34
+ [5.14.10]
35
+ * Use custom namespace for our packages - @cloudron/manifest-format , @cloudron/superagent
36
+ * add install `--memory-limit`
37
+
38
+ [5.15.0]
39
+ * decrypt-dir: option to disable filename decryption
40
+ * Add `--env` to install
41
+
42
+ [5.16.0]
43
+ * build: output the selected Dockerfile name
44
+
package/bin/cloudron CHANGED
@@ -56,6 +56,7 @@ backupCommand.command('decrypt <infile> <outfile>')
56
56
  backupCommand.command('decrypt-dir <indir> <outdir>')
57
57
  .description('Decrypt an encrypted directory')
58
58
  .option('--password <password>', 'password')
59
+ .option('--no-decrypt-filenames', 'Decrypt filenames [false]')
59
60
  .action(backupTools.decryptDir);
60
61
 
61
62
  backupCommand.command('decrypt-filename <path>')
@@ -111,7 +112,7 @@ envCommand.command('list')
111
112
  .action(actions.envList);
112
113
 
113
114
  envCommand.command('set <KEY=value...>')
114
- .description('Set environment variables')
115
+ .description('Set environment variables. e.g X=1 Y=2')
115
116
  .option('--app <id>', 'App id')
116
117
  .action(actions.envSet);
117
118
 
@@ -173,6 +174,7 @@ program.command('install')
173
174
  .option('--no-sso', 'Disable Cloudron SSO [false]')
174
175
  .option('--debug [cmd...]', 'Enable debug mode', false)
175
176
  .option('--readonly', 'Mount filesystem readonly. Default is read/write in debug mode.')
177
+ .option('--env <KEY=value...>', 'Set environment variables. e.g X=1 Y=2')
176
178
  .action(actions.install);
177
179
 
178
180
  program.command('list')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "5.14.10",
3
+ "version": "5.16.0",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "main": "main.js",
@@ -17,25 +17,25 @@
17
17
  },
18
18
  "author": "Cloudron Developers <support@cloudron.io>",
19
19
  "dependencies": {
20
- "@cloudron/manifest-format": "^5.27.0",
20
+ "@cloudron/manifest-format": "^5.28.0",
21
21
  "@cloudron/superagent": "^1.0.0",
22
22
  "commander": "^13.1.0",
23
- "debug": "^4.4.0",
23
+ "debug": "^4.4.1",
24
24
  "easy-table": "^1.2.0",
25
25
  "ejs": "^3.1.10",
26
- "eventsource": "^3.0.6",
26
+ "eventsource": "^3.0.7",
27
27
  "micromatch": "^4.0.8",
28
- "open": "^10.1.1",
28
+ "open": "^10.2.0",
29
29
  "safetydance": "^2.5.1",
30
- "tar-fs": "^3.0.8"
30
+ "tar-fs": "^3.1.0"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">= 18.x.x"
34
34
  },
35
35
  "devDependencies": {
36
- "@eslint/js": "^9.25.1",
37
- "eslint": "^9.25.1",
36
+ "@eslint/js": "^9.32.0",
37
+ "eslint": "^9.32.0",
38
38
  "expect.js": "^0.3.1",
39
- "mocha": "^11.1.0"
39
+ "mocha": "^11.7.1"
40
40
  }
41
41
  }
package/src/actions.js CHANGED
@@ -643,6 +643,16 @@ async function install(localOptions, cmd) {
643
643
 
644
644
  for (const port in ports) console.log(`Port ${port}: ${ports[port]}`);
645
645
 
646
+ // environment variables
647
+ const env = {};
648
+ if (options.env) {
649
+ options.env.forEach(envVar => {
650
+ const m = envVar.match(/(.*?)=(.*)/);
651
+ if (!m) return exit(`Expecting KEY=VALUE pattern. Got ${envVar}`);
652
+ env[m[1]] = m[2];
653
+ });
654
+ }
655
+
646
656
  const memoryLimit = options.memoryLimit ? parseMemoryLimit(options.memoryLimit) : 0;
647
657
 
648
658
  const data = {
@@ -655,7 +665,8 @@ async function install(localOptions, cmd) {
655
665
  aliasDomains,
656
666
  ports,
657
667
  accessRestriction: null,
658
- memoryLimit
668
+ memoryLimit,
669
+ env
659
670
  };
660
671
 
661
672
  // the sso only applies for apps which allow optional sso
@@ -1227,10 +1238,12 @@ async function importApp(localOptions, cmd) {
1227
1238
  }
1228
1239
 
1229
1240
  if (backupKey) data.backupConfig.key = backupKey;
1241
+ } else {
1242
+ data.inPlace = true;
1230
1243
  }
1231
1244
 
1232
1245
  const request = createRequest('POST', `/api/v1/apps/${app.id}/import`, options);
1233
- const response = await request.send({});
1246
+ const response = await request.send(data);
1234
1247
  if (response.status !== 202) return exit(`Failed to import app: ${requestError(response)}`);
1235
1248
 
1236
1249
  await waitForFinishInstallation(app.id, response.body.taskId, options);
@@ -262,11 +262,13 @@ async function decryptDir(inDir, outDir, options) {
262
262
  }
263
263
 
264
264
  const encryption = aesKeysFromPassword(options.password);
265
+ const decryptFilenames = options.decryptFilenames;
265
266
 
266
267
  const inDirAbs = path.resolve(process.cwd(), inDir);
267
268
  const outDirAbs = path.resolve(process.cwd(), outDir);
268
269
 
269
270
  const tbd = [ '' ]; // only has paths relative to inDirAbs
271
+ let errorCount = 0;
270
272
  while (true) {
271
273
  const cur = tbd.pop();
272
274
  const entries = fs.readdirSync(path.join(inDirAbs, cur), { withFileTypes: true });
@@ -279,29 +281,41 @@ async function decryptDir(inDir, outDir, options) {
279
281
  continue;
280
282
  }
281
283
 
282
- const encryptedFilePath = path.join(cur, entry.name);
283
-
284
- const { error, decryptedFilePath } = decryptFilePath(encryptedFilePath, encryption);
285
- if (error) throw error;
284
+ const fullPath = path.join(cur, entry.name);
285
+
286
+ let destPath = fullPath;
287
+ if (decryptFilenames) {
288
+ const { error, decryptedFilePath } = decryptFilePath(fullPath, encryption);
289
+ if (error) {
290
+ console.warn(`Could not decrypt filename: ${error.message}`);
291
+ errorCount++;
292
+ continue;
293
+ }
294
+ destPath = decryptedFilePath;
295
+ }
286
296
 
287
- console.log(`Decrypting ${decryptedFilePath}`);
297
+ console.log(`Decrypting ${destPath}`);
288
298
 
289
299
  const infile = path.join(inDirAbs, cur, entry.name);
290
300
  const inStream = fs.createReadStream(infile);
291
- fs.mkdirSync(path.dirname(path.join(outDirAbs, decryptedFilePath)), { recursive: true });
292
- const outfile = path.join(outDirAbs, decryptedFilePath);
301
+ fs.mkdirSync(path.dirname(path.join(outDirAbs, destPath)), { recursive: true });
302
+ const outfile = path.join(outDirAbs, destPath);
293
303
  const outStream = fs.createWriteStream(outfile);
294
304
  const decryptStream = new DecryptStream(encryption);
295
305
 
296
306
  const [decryptError] = await safe(stream.pipeline(inStream, decryptStream, outStream));
297
307
  if (decryptError) {
298
308
  safe.fs.rmSync(outfile);
299
- throw new Error(`Could not decrypt ${infile}: ${decryptError.message}`);
309
+ console.warn(`Could not decrypt ${infile}: ${decryptError.message}`);
310
+ ++errorCount;
300
311
  }
301
312
  }
302
313
 
303
314
  if (tbd.length === 0) break;
304
315
  }
316
+
317
+
318
+ if (errorCount) exit(`Failed to decrypt ${errorCount} files`);
305
319
  }
306
320
 
307
321
  async function decryptFilename(filePath, options) {
@@ -25,7 +25,7 @@ const assert = require('assert'),
25
25
  stream = require('stream/promises'),
26
26
  superagent = require('@cloudron/superagent'),
27
27
  tar = require('tar-fs'),
28
- url = require('url');
28
+ { URL } = require('url');
29
29
 
30
30
  function requestError(response) {
31
31
  if (response.status === 401 || response.status === 403) return 'Invalid token. Use cloudron build login again.';
@@ -86,11 +86,16 @@ async function followBuildLog(buildId, raw) {
86
86
  assert.strictEqual(typeof buildId, 'string');
87
87
  assert.strictEqual(typeof raw, 'boolean');
88
88
 
89
- // EventSource always requires http
90
- let tmp = url.parse(config.getBuildServiceConfig().url);
91
- if (tmp.protocol !== 'https:' && tmp.protocol !== 'http:') tmp = url.parse('http://' + config.getBuildServiceConfig().url);
89
+ const rawUrl = config.getBuildServiceConfig().url;
90
+ let tmp;
91
+ try {
92
+ tmp = new URL(rawUrl);
93
+ } catch {
94
+ tmp = new URL(`http://${rawUrl}`); // if it fails, prepend http://
95
+ }
96
+
97
+ const es = new EventSource(`${tmp.origin}/api/v1/builds/${buildId}/logstream?accessToken=${config.getBuildServiceConfig().token}`);
92
98
 
93
- const es = new EventSource(`${tmp.href}api/v1/builds/${buildId}/logstream?accessToken=${config.getBuildServiceConfig().token}`);
94
99
  let prevId = null, prevWasStatus = false;
95
100
 
96
101
  es.addEventListener('message', function (e) {
@@ -195,16 +200,16 @@ async function buildLocal(manifest, sourceDir, appConfig, options) {
195
200
  }
196
201
 
197
202
  const dockerImage = `${appConfig.repository}:${tag}`;
198
-
199
- console.log('Building locally as %s', dockerImage);
200
- console.log();
201
-
202
203
  const buildArgsCmdLine = options.buildArg.map(function (a) { return `--build-arg "${a}"`; }).join(' ');
203
204
 
204
205
  let dockerfile = 'Dockerfile';
205
206
  if (options.file) dockerfile = options.file;
206
207
  else if (fs.existsSync(`${sourceDir}/Dockerfile.cloudron`)) dockerfile = 'Dockerfile.cloudron';
207
208
  else if (fs.existsSync(`${sourceDir}/cloudron/Dockerfile`)) dockerfile = 'cloudron/Dockerfile';
209
+
210
+ console.log(`Building ${dockerfile} locally as ${dockerImage}`);
211
+ console.log();
212
+
208
213
  execSync(`docker build ${!options.cache ? '--no-cache' : ''} -t ${dockerImage} -f ${dockerfile} ${buildArgsCmdLine} ${sourceDir}`, { stdio: 'inherit' });
209
214
 
210
215
  if (options.push) {
@@ -240,13 +245,20 @@ async function buildRemote(manifest, sourceDir, appConfig, options) {
240
245
 
241
246
  const dockerImage = `${appConfig.repository}:${tag}`;
242
247
 
243
- console.log('Building %s', dockerImage);
248
+ let dockerfile = 'Dockerfile';
249
+ if (options.file) dockerfile = options.file;
250
+ else if (fs.existsSync(`${sourceDir}/Dockerfile.cloudron`)) dockerfile = 'Dockerfile.cloudron';
251
+ else if (fs.existsSync(`${sourceDir}/cloudron/Dockerfile`)) dockerfile = 'cloudron/Dockerfile';
252
+
253
+ const buildServiceConfig = config.getBuildServiceConfig();
254
+
255
+ console.log(`Building ${dockerfile} as ${dockerImage}`);
244
256
 
245
257
  const sourceArchiveFilePath = path.join(os.tmpdir(), path.basename(sourceDir) + '.tar.gz');
246
258
  const dockerignoreFilePath = path.join(sourceDir, '.dockerignore');
247
259
  const ignoreMatcher = dockerignoreMatcher(dockerignoreFilePath);
248
260
 
249
- console.log('Uploading source tarball...');
261
+ console.log(`Uploading source tarball to ${buildServiceConfig.url} ...`);
250
262
 
251
263
  const tarStream = tar.pack(sourceDir, {
252
264
  ignore: function (name) {
@@ -258,11 +270,6 @@ async function buildRemote(manifest, sourceDir, appConfig, options) {
258
270
  const [tarError] = await safe(stream.pipeline(tarStream, sourceArchiveStream));
259
271
  if (tarError) return exit(`Could not tar: ${tarError.message}`);
260
272
 
261
- let dockerfile = 'Dockerfile';
262
- if (options.file) dockerfile = options.file;
263
- else if (fs.existsSync(`${sourceDir}/Dockerfile.cloudron`)) dockerfile = 'Dockerfile.cloudron';
264
- else if (fs.existsSync(`${sourceDir}/cloudron/Dockerfile`)) dockerfile = 'cloudron/Dockerfile';
265
-
266
273
  const buildArgsObject = {};
267
274
  options.buildArg.forEach(function (a) {
268
275
  const key = a.slice(0, a.indexOf('='));
@@ -270,7 +277,6 @@ async function buildRemote(manifest, sourceDir, appConfig, options) {
270
277
  buildArgsObject[key] = value;
271
278
  });
272
279
 
273
- const buildServiceConfig = config.getBuildServiceConfig();
274
280
  const response = await superagent.post(`${buildServiceConfig.url}/api/v1/builds`)
275
281
  .query({ accessToken: buildServiceConfig.token, noCache: !options.cache, dockerfile: dockerfile, noPush: !options.push })
276
282
  .field('dockerImageRepo', appConfig.repository)