elestio 1.2.3 → 1.3.1

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/README.md CHANGED
@@ -249,7 +249,7 @@ replica.
249
249
  | `elestio backups remote-list <vmID>` | List remote backups |
250
250
  | `elestio backups remote-take <vmID>` | Take remote backup |
251
251
  | `elestio backups remote-restore <vmID> <snapshot>` | Restore remote backup |
252
- | `elestio backups auto-enable <vmID>` | Setup auto backups |
252
+ | `elestio backups auto-enable <vmID> [--path /opt] [--hour 03:00]` | Setup auto backups of a directory on the VM (default `/opt`) |
253
253
  | `elestio backups auto-disable <vmID>` | Disable auto backups |
254
254
 
255
255
  ### Snapshots
@@ -345,17 +345,24 @@ written as `random_password` are generated, `[EMAIL]` becomes your account
345
345
  email, and `[CI_CD_DOMAIN]` is resolved by the platform once the pipeline has a
346
346
  domain.
347
347
 
348
- **Two routes:**
348
+ **Three routes. The default needs nothing from you:**
349
349
 
350
- | | compose (default) | git (`--owner <git-user>`) |
351
- |---|---|---|
352
- | What it does | Inlines the template's `docker-compose.yml` | Generates the template repo into your Git account, then builds from it |
353
- | Needs a Git account | No | Yes, connected in the dashboard |
354
- | Lifecycle scripts | Skipped | Run |
355
- | Repo files the compose mounts | Unavailable | Available |
356
- | Works for | Templates that need no files from the repo | Every template |
350
+ | | repo (default) | inline (`--inline-compose`) | git (`--owner <git-user>`) |
351
+ |---|---|---|---|
352
+ | What it does | Points the pipeline at the `elestio-examples` repo, which Elestio clones on the VM | Inlines the template's `docker-compose.yml` only | Generates the template repo into your Git account, then builds from it |
353
+ | Needs a Git account | No | No | Yes, connected in the dashboard |
354
+ | Install scripts | **Run** | Skipped | Run |
355
+ | Files the compose mounts or reads | **Present** | Missing | Present |
356
+ | Works for | The whole catalogue | Templates that need no file from the repo | Every template |
357
+
358
+ The default route sends `imageData.dockerExample` (the template repo URL),
359
+ `branch` and `repoName`. The backend stores them as the pipeline's
360
+ `gitConfig.repoURL`, and the deployment agent clones that repo with Elestio's
361
+ own Git token - the same thing the dashboard does. Nothing is copied into your
362
+ account, and no Git account is needed.
357
363
 
358
- The compose route has no checkout and runs no install script, so a template
364
+ `--inline-compose` sends only the compose file, which is what you want for a
365
+ compose of your own. That route has no checkout and runs no install script, so a template
359
366
  whose `docker-compose.yml` bind-mounts a file cannot work: Docker creates the
360
367
  missing source as an empty directory and the container fails to start. The file
361
368
  may ship in the template repo (n8n's `./n8n-task-runners.json`) or be generated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elestio",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "Elestio CLI - Deploy and manage services on the Elestio DevOps platform",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
@@ -83,8 +83,13 @@ export async function restoreRemoteBackup(vmID, snapshotName) {
83
83
  return response;
84
84
  }
85
85
 
86
- export async function setupAutoBackups(vmID, backupPath = '/backup/', backupHour = '03:00') {
87
- log('info', `Setting up auto backups for ${vmID}...`);
86
+ // Directory the backend hands to `borg create` on the VM. It must exist there,
87
+ // or every archive is empty. /opt holds the app stack; the dashboard sends it.
88
+ export const DEFAULT_BACKUP_PATH = '/opt';
89
+
90
+ export async function setupAutoBackups(vmID, backupPath = DEFAULT_BACKUP_PATH, backupHour = '03:00') {
91
+ if (!backupPath.startsWith('/')) throw new Error(`Backup path must be absolute, got "${backupPath}"`);
92
+ log('info', `Setting up auto backups for ${vmID} (${backupPath})...`);
88
93
  const response = await apiRequest('/api/backups/SetupAutoBackups', 'POST', { serverID: String(vmID), backupPath, backupHour });
89
94
  if (response.status !== 'OK') throw new Error(response.message || 'Failed to setup');
90
95
  log('success', `Auto backups configured at ${backupHour}`);
@@ -3,7 +3,7 @@ import { loadConfig } from '../config.js';
3
3
  import { log, colors, outputJson, formatTable } from '../utils.js';
4
4
  import { getTemplates } from './templates.js';
5
5
  import { createSubstitutions, normalizeElestioConfig, repoFileMounts } from '../templates/elestio-config.js';
6
- import { buildGitPipelinePayload, buildComposePipelinePayload } from '../payloads/pipeline.js';
6
+ import { buildGitPipelinePayload, buildComposePipelinePayload, buildTemplateRepoPipelinePayload } from '../payloads/pipeline.js';
7
7
  import { parseGitAuthId } from './cicd.js';
8
8
  import { TEMPLATE_REPO_OWNER, TEMPLATE_REPO_HOST } from '../constants.js';
9
9
 
@@ -16,11 +16,18 @@ import { TEMPLATE_REPO_OWNER, TEMPLATE_REPO_HOST } from '../constants.js';
16
16
  * produces a pipeline that starts nothing, which is why this is a dedicated
17
17
  * command rather than a flag on `cicd create`.
18
18
  *
19
- * Two routes:
20
- * git - generate the template repo into the user's own Git account, then
21
- * point the pipeline at it. Keeps the lifecycle scripts.
22
- * compose - inline the template's docker-compose.yml. No Git account needed,
23
- * but lifecycle scripts are lost because there is no checkout.
19
+ * Three routes:
20
+ * repo (default) - point the pipeline at the elestio-examples repo itself.
21
+ * Elestio clones it on the VM with its own Git token, so the repo's
22
+ * files exist and its install scripts run. No Git account needed,
23
+ * nothing copied into the user's account. This is what the
24
+ * dashboard does.
25
+ * inline - inline the docker-compose.yml only (--inline-compose). No
26
+ * checkout: install scripts are skipped and any file the compose
27
+ * mounts or reads (env_file) is missing. Half the catalogue cannot
28
+ * work this way.
29
+ * git - generate the template repo into the user's own Git account
30
+ * (--owner). Needs an endpoint the API does not expose (404).
24
31
  */
25
32
 
26
33
  function templateRepoUrl(repoName) {
@@ -208,13 +215,16 @@ export async function deployTemplate(nameOrId, options = {}) {
208
215
  }
209
216
  log('success', `Config loaded: runtime "${elestioConfig.config.runTime}", ${elestioConfig.variables ? elestioConfig.variables.split('\n').length : 0} env vars, ${elestioConfig.ports.length} port(s)`);
210
217
 
211
- const payload = useGit
218
+ const route = useGit ? 'git' : (options.inlineCompose ? 'inline' : 'repo');
219
+ const payload = route === 'git'
212
220
  ? await buildGitRoute({ options, projectId, target, template, repoName, pipelineName, branch, elestioConfig, dryRun: !!options.dryRun })
213
- : await buildComposeRoute({ projectId, target, repoName, pipelineName, branch, sourceRepoUrl, elestioConfig, force: options.force });
221
+ : route === 'inline'
222
+ ? await buildComposeRoute({ projectId, target, repoName, pipelineName, branch, sourceRepoUrl, elestioConfig, force: options.force })
223
+ : await buildTemplateRepoRoute({ projectId, target, repoName, pipelineName, branch, sourceRepoUrl, elestioConfig });
214
224
 
215
225
  if (options.dryRun) {
216
- if (options.json) { outputJson({ dryRun: true, route: useGit ? 'git' : 'compose', payload }); return payload; }
217
- printPlan({ template, repoName, pipelineName, target, elestioConfig, useGit, branch });
226
+ if (options.json) { outputJson({ dryRun: true, route, payload }); return payload; }
227
+ printPlan({ template, repoName, pipelineName, target, elestioConfig, route, branch });
218
228
  log('info', 'To deploy, run the same command without --dry-run');
219
229
  return payload;
220
230
  }
@@ -229,7 +239,7 @@ export async function deployTemplate(nameOrId, options = {}) {
229
239
  log('success', `Pipeline "${pipelineName}" created on ${target.displayName}`);
230
240
 
231
241
  if (options.json) {
232
- outputJson({ status: 'OK', pipelineName, target: target.vmID, route: useGit ? 'git' : 'compose', webUI: elestioConfig.webUI, response });
242
+ outputJson({ status: 'OK', pipelineName, target: target.vmID, route, webUI: elestioConfig.webUI, response });
233
243
  return response;
234
244
  }
235
245
 
@@ -286,6 +296,22 @@ async function buildGitRoute({ options, projectId, target, template, repoName, p
286
296
  });
287
297
  }
288
298
 
299
+ /**
300
+ * Default route: the pipeline points at the template repo, which Elestio clones
301
+ * on the VM. Nothing to refuse -- the repo's files are there and its install
302
+ * scripts run.
303
+ */
304
+ async function buildTemplateRepoRoute({ projectId, target, repoName, pipelineName, branch, sourceRepoUrl, elestioConfig }) {
305
+ log('info', `Fetching docker-compose from ${TEMPLATE_REPO_OWNER}/${repoName}...`);
306
+ const compose = await fetchCompose(sourceRepoUrl, branch, projectId);
307
+ log('success', 'Compose file loaded');
308
+
309
+ const hooks = installHooks(elestioConfig);
310
+ if (hooks.length > 0) log('info', `Install scripts run from the cloned repo: ${hooks.join(', ')}`);
311
+
312
+ return buildTemplateRepoPipelinePayload({ target, projectId, pipelineName, compose, repoName, branch, elestioConfig });
313
+ }
314
+
289
315
  async function buildComposeRoute({ projectId, target, repoName, pipelineName, branch, sourceRepoUrl, elestioConfig, force }) {
290
316
  log('info', `Fetching docker-compose from ${TEMPLATE_REPO_OWNER}/${repoName}...`);
291
317
  const compose = await fetchCompose(sourceRepoUrl, branch, projectId);
@@ -346,13 +372,18 @@ function collectOverrides(options) {
346
372
  return overrides;
347
373
  }
348
374
 
349
- function printPlan({ template, repoName, pipelineName, target, elestioConfig, useGit, branch }) {
375
+ function printPlan({ template, repoName, pipelineName, target, elestioConfig, route, branch }) {
350
376
  console.log(`\n${colors.bold}Pipeline Preview (--dry-run)${colors.reset}\n`);
351
377
  console.log(` Software: ${colors.cyan}${template.title}${colors.reset} (ID: ${template.id})`);
352
378
  console.log(` Template: ${TEMPLATE_REPO_OWNER}/${repoName} @ ${branch}`);
353
379
  console.log(` Pipeline: ${pipelineName}`);
354
380
  console.log(` Target: ${target.displayName} (${target.vmID})`);
355
- console.log(` Route: ${useGit ? 'git (repo generated in your account, lifecycle scripts kept)' : 'compose (inlined, lifecycle scripts skipped)'}`);
381
+ const routes = {
382
+ repo: 'repo (Elestio clones the template repo on the VM, install scripts run)',
383
+ inline: 'inline compose (no checkout: install scripts skipped, mounted files missing)',
384
+ git: 'git (repo generated in your account, install scripts run)'
385
+ };
386
+ console.log(` Route: ${routes[route]}`);
356
387
  console.log(` Runtime: ${elestioConfig.config.runTime || 'N/A'}`);
357
388
  console.log(` Ports: ${elestioConfig.ports.map(p => `${p.listeningPort}->${p.targetPort}`).join(', ') || 'default'}`);
358
389
  console.log(` Env vars: ${elestioConfig.variables ? elestioConfig.variables.split('\n').map(v => v.split('=')[0]).join(', ') : 'none'}`);
@@ -1,4 +1,4 @@
1
- import { RUNTIME_PRESETS } from '../constants.js';
1
+ import { RUNTIME_PRESETS, TEMPLATE_REPO_OWNER, TEMPLATE_REPO_HOST } from '../constants.js';
2
2
  import { EMPTY_LIFECYCLE } from '../templates/elestio-config.js';
3
3
 
4
4
  /**
@@ -147,6 +147,55 @@ export function buildGitPipelinePayload({
147
147
  });
148
148
  }
149
149
 
150
+ /**
151
+ * Pipeline running catalog software from its own template repo.
152
+ *
153
+ * This is what the dashboard sends: `imageData.dockerExample` carries the
154
+ * elestio-examples URL, and the backend then stores it as the pipeline's
155
+ * gitConfig.repoURL (createCiCdExistServer). The deployment agent clones that
156
+ * repo on the VM with Elestio's own Git token, so the repo's files exist and
157
+ * its lifecycle scripts run -- without copying anything into the user's Git
158
+ * account, and without needing a connected Git account at all.
159
+ *
160
+ * The inline route below cannot do either, which is why half the catalogue
161
+ * (Kafka, Chromadb, Weaviate, ClickHouse, WordPress...) is undeployable
162
+ * through it.
163
+ */
164
+ export function buildTemplateRepoPipelinePayload({
165
+ target, projectId, pipelineName, compose, repoName, branch = 'main', elestioConfig, overrides = {}
166
+ }) {
167
+ if (!compose) throw new Error('A docker-compose file is required');
168
+ if (!repoName) throw new Error('A template repo name is required');
169
+ const fromFile = elestioConfig && elestioConfig.hasConfig ? elestioConfig : null;
170
+
171
+ return buildPipelinePayload({
172
+ target, projectId, pipelineName,
173
+ cicdMode: 'DockerCompose',
174
+ gitData: {},
175
+ imageData: {
176
+ isPrivate: false,
177
+ compose,
178
+ // The three fields that turn this into a cloned-repo pipeline.
179
+ dockerExample: `${TEMPLATE_REPO_HOST}/${TEMPLATE_REPO_OWNER}/${repoName}`,
180
+ branch,
181
+ repoName
182
+ },
183
+ configData: {
184
+ runTime: 'NodeJs', framework: 'NoFramework', version: '20',
185
+ buildDir: '/', rootDir: '/', buildCmd: '', runCmd: '', installCmd: ''
186
+ },
187
+ ports: overrides.ports || fromFile?.ports,
188
+ exposedPorts: overrides.exposedPorts || fromFile?.exposedPorts,
189
+ variables: overrides.variables ?? fromFile?.variables ?? '',
190
+ // Kept, unlike the inline route: the repo is checked out, so they can run.
191
+ lifeCycleCommand: fromFile?.lifeCycleCommand,
192
+ copyCommandConfig: fromFile?.copyCommandConfig || [],
193
+ authID: null,
194
+ isPublicGitRepo: false,
195
+ isNeedToCreateRepo: false
196
+ });
197
+ }
198
+
150
199
  /**
151
200
  * Pipeline running an inline docker-compose file, with no Git repo attached.
152
201
  *
package/src/registry.js CHANGED
@@ -504,7 +504,7 @@ export const registry = {
504
504
  'remote-list': { summary: 'List remote backups', usage: 'remote-list <vmID>', async run({ args, json }) { await (await load.backups()).listRemoteBackups(requireArg(args._[2], 'backups remote-list <vmID>'), json); } },
505
505
  'remote-take': { summary: 'Take a remote backup', usage: 'remote-take <vmID>', async run({ args }) { await (await load.backups()).takeRemoteBackup(requireArg(args._[2], 'backups remote-take <vmID>')); } },
506
506
  'remote-restore': { summary: 'Restore a remote backup', usage: 'remote-restore <vmID> <snapshot>', async run({ args }) { await (await load.backups()).restoreRemoteBackup(requireArg(args._[2], 'backups remote-restore <vmID> <snapshot>'), positional(args, 3, 'snapshot')); } },
507
- 'auto-enable': { summary: 'Enable scheduled backups (--path, --hour)', usage: 'auto-enable <vmID>', async run({ args }) { await (await load.backups()).setupAutoBackups(requireArg(args._[2], 'backups auto-enable <vmID>'), args.path, args.hour); } },
507
+ 'auto-enable': { summary: 'Enable scheduled backups (--path dir on the VM, default /opt; --hour)', usage: 'auto-enable <vmID>', async run({ args }) { await (await load.backups()).setupAutoBackups(requireArg(args._[2], 'backups auto-enable <vmID>'), args.path, args.hour); } },
508
508
  'auto-disable': { summary: 'Disable scheduled backups', usage: 'auto-disable <vmID>', async run({ args }) { await (await load.backups()).disableAutoBackups(requireArg(args._[2], 'backups auto-disable <vmID>')); } }
509
509
  }
510
510
  },
@@ -632,6 +632,7 @@ export const registry = {
632
632
  owner: args.owner, branch: args.branch, repoName: args['repo-name'],
633
633
  gitType: args['git-type'], authId: args['auth-id'],
634
634
  git: !args['no-git'] && (args.git === true || args.git === 'true' || !!args.owner),
635
+ inlineCompose: !!args['inline-compose'],
635
636
  private: !!args.private, nonOrg: !!args['non-org'],
636
637
  buildCmd: args['build-cmd'], runCmd: args['run-cmd'],
637
638
  installCmd: args['install-cmd'], buildDir: args['build-dir'],
@@ -185,8 +185,6 @@ export function repoFileMounts(compose) {
185
185
  };
186
186
 
187
187
  // Short syntax, quoted or not: - ./file:/in/container[:ro]
188
- // Many templates quote their mounts (Chromadb among them), and the
189
- // unquoted-only pattern let those through silently.
190
188
  for (const match of compose.matchAll(/^\s*-\s+["']?(\.{1,2}\/[^\s:"']+)["']?\s*:/gm)) {
191
189
  add(match[1]);
192
190
  }
@@ -196,5 +194,19 @@ export function repoFileMounts(compose) {
196
194
  add(match[1]);
197
195
  }
198
196
 
197
+ // env_file, inline or as a list. Docker refuses to start when the file is
198
+ // missing, and 8 of the 22 catalogue templates sampled read one.
199
+ for (const match of compose.matchAll(/^\s*env_file:\s*(?:\[([^\]]*)\]|["']?([^\s"'\[]+)["']?)\s*$/gm)) {
200
+ for (const entry of (match[1] ?? match[2] ?? '').split(',')) {
201
+ const file = entry.trim().replace(/^["']|["']$/g, '');
202
+ if (file.startsWith('.')) add(file);
203
+ }
204
+ }
205
+ for (const block of compose.matchAll(/^\s*env_file:\s*$((?:\n\s*-\s+.*)+)/gm)) {
206
+ for (const item of block[1].matchAll(/^\s*-\s+["']?([^\s"']+)["']?\s*$/gm)) {
207
+ if (item[1].startsWith('.')) add(item[1]);
208
+ }
209
+ }
210
+
199
211
  return [...mounts];
200
212
  }