ninedeploy 0.18.25 → 0.18.27

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.
Files changed (74) hide show
  1. package/dist/commands/databaseUpgrade.d.ts +25 -0
  2. package/dist/commands/databaseUpgrade.d.ts.map +1 -0
  3. package/dist/commands/databaseUpgrade.js +136 -0
  4. package/dist/commands/databaseUpgrade.js.map +1 -0
  5. package/dist/commands/importApply.d.ts +114 -0
  6. package/dist/commands/importApply.d.ts.map +1 -0
  7. package/dist/commands/importApply.js +403 -0
  8. package/dist/commands/importApply.js.map +1 -0
  9. package/dist/commands/importCutover.d.ts +95 -0
  10. package/dist/commands/importCutover.d.ts.map +1 -0
  11. package/dist/commands/importCutover.js +293 -0
  12. package/dist/commands/importCutover.js.map +1 -0
  13. package/dist/commands/importData.d.ts +60 -0
  14. package/dist/commands/importData.d.ts.map +1 -0
  15. package/dist/commands/importData.js +286 -0
  16. package/dist/commands/importData.js.map +1 -0
  17. package/dist/commands/importPlan.d.ts +32 -0
  18. package/dist/commands/importPlan.d.ts.map +1 -0
  19. package/dist/commands/importPlan.js +138 -0
  20. package/dist/commands/importPlan.js.map +1 -0
  21. package/dist/commands/importVolumes.d.ts +38 -0
  22. package/dist/commands/importVolumes.d.ts.map +1 -0
  23. package/dist/commands/importVolumes.js +161 -0
  24. package/dist/commands/importVolumes.js.map +1 -0
  25. package/dist/index.js +59 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/importers/caprover.d.ts +13 -0
  28. package/dist/lib/importers/caprover.d.ts.map +1 -0
  29. package/dist/lib/importers/caprover.js +150 -0
  30. package/dist/lib/importers/caprover.js.map +1 -0
  31. package/dist/lib/importers/connections.d.ts +39 -0
  32. package/dist/lib/importers/connections.d.ts.map +1 -0
  33. package/dist/lib/importers/connections.js +119 -0
  34. package/dist/lib/importers/connections.js.map +1 -0
  35. package/dist/lib/importers/coolify.d.ts +10 -0
  36. package/dist/lib/importers/coolify.d.ts.map +1 -0
  37. package/dist/lib/importers/coolify.js +174 -0
  38. package/dist/lib/importers/coolify.js.map +1 -0
  39. package/dist/lib/importers/dokploy.d.ts +13 -0
  40. package/dist/lib/importers/dokploy.d.ts.map +1 -0
  41. package/dist/lib/importers/dokploy.js +183 -0
  42. package/dist/lib/importers/dokploy.js.map +1 -0
  43. package/dist/lib/importers/heroku.d.ts +50 -0
  44. package/dist/lib/importers/heroku.d.ts.map +1 -0
  45. package/dist/lib/importers/heroku.js +159 -0
  46. package/dist/lib/importers/heroku.js.map +1 -0
  47. package/dist/lib/importers/plan.d.ts +137 -0
  48. package/dist/lib/importers/plan.d.ts.map +1 -0
  49. package/dist/lib/importers/plan.js +110 -0
  50. package/dist/lib/importers/plan.js.map +1 -0
  51. package/dist/lib/importers/shell.d.ts +24 -0
  52. package/dist/lib/importers/shell.d.ts.map +1 -0
  53. package/dist/lib/importers/shell.js +45 -0
  54. package/dist/lib/importers/shell.js.map +1 -0
  55. package/dist/lib/importers/sources.d.ts +26 -0
  56. package/dist/lib/importers/sources.d.ts.map +1 -0
  57. package/dist/lib/importers/sources.js +100 -0
  58. package/dist/lib/importers/sources.js.map +1 -0
  59. package/package.json +3 -3
  60. package/src/commands/databaseUpgrade.ts +124 -0
  61. package/src/commands/importApply.ts +445 -0
  62. package/src/commands/importCutover.ts +329 -0
  63. package/src/commands/importData.ts +302 -0
  64. package/src/commands/importPlan.ts +143 -0
  65. package/src/commands/importVolumes.ts +168 -0
  66. package/src/index.ts +61 -0
  67. package/src/lib/importers/caprover.ts +141 -0
  68. package/src/lib/importers/connections.ts +133 -0
  69. package/src/lib/importers/coolify.ts +164 -0
  70. package/src/lib/importers/dokploy.ts +167 -0
  71. package/src/lib/importers/heroku.ts +168 -0
  72. package/src/lib/importers/plan.ts +209 -0
  73. package/src/lib/importers/shell.ts +56 -0
  74. package/src/lib/importers/sources.ts +96 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * `ninedeploy import data <plan> --volume <service>=<file.tar.gz>` (A1 step 3).
3
+ *
4
+ * Moves the contents of a service's data volume: the volume mounted at the
5
+ * path the service was created with (`create.volumeMount`), which NineDeploy
6
+ * keeps in `nd-svc-<slug>-data`.
7
+ *
8
+ * - The command printed for the source host mounts a named volume by its
9
+ * name, read-only (no container needed: a Swarm service scaled to 0 has
10
+ * none); a bind mount is read through the source container instead. It
11
+ * writes a gzipped tar with numeric owners, the format the panel restores.
12
+ * - The archive goes through the panel's chunked volume import with its
13
+ * sha256, which the panel checks before the volume is touched. The panel
14
+ * snapshots a volume that already exists, then swaps the archive in.
15
+ * - The receipt records the import id, sha256 and size. A stopped upload
16
+ * resumes, a volume already imported is not imported again.
17
+ */
18
+ import path from 'node:path';
19
+ import { fileBase, flagArg, isPlainName, shellQuote, sourceTarget } from '../lib/importers/shell.js';
20
+ /** The helper image the panel itself tars volumes with (server lib/inventory.ts HELPER_IMAGE). */
21
+ const TAR_IMAGE = 'alpine:3.21';
22
+ /**
23
+ * The source-host command that archives a service's data volume, or why it
24
+ * cannot be printed. Null when the service has no data volume.
25
+ */
26
+ export function volumeCommand(s) {
27
+ const mount = s.create.volumeMount;
28
+ if (!mount)
29
+ return null;
30
+ const v = s.volumes.find((x) => x.mountPath === mount);
31
+ if (v?.source === 'file')
32
+ return { manual: `${mount} is a file mount on the source; recreate it under Service → File mounts` };
33
+ const notes = [];
34
+ const others = s.volumes.filter((x) => x.mountPath !== mount && x.source !== 'file');
35
+ if (others.length)
36
+ notes.push(`Only ${mount} moves; copy ${others.map((x) => x.mountPath).join(', ')} by hand.`);
37
+ const file = `${fileBase(s.name)}.volume.tar.gz`;
38
+ // A named volume is mounted by its name: that needs no container, and a
39
+ // Swarm service scaled to 0 has none left (checked live on CapRover 1.16,
40
+ // which names the Docker volume exactly as the app's volumeName).
41
+ if (v?.source === 'volume' && v.name && isPlainName(v.name)) {
42
+ return { command: `docker run --rm -v ${v.name}:/v:ro ${TAR_IMAGE} tar -czf - --numeric-owner -C /v . > ${file}`, file, notes };
43
+ }
44
+ // A bind mount, or a volume whose name the plan does not hold: borrow the
45
+ // source container's mounts, which works only while it still exists.
46
+ const { target, note } = sourceTarget(s.runtime, `${s.name}'s container`, true);
47
+ if (note)
48
+ notes.push(note);
49
+ notes.push('This reads the mount through the source container, so stop the app without removing its container (a Swarm service scaled to 0 has none left); or archive the host directory yourself with tar -czf - --numeric-owner -C <dir> .');
50
+ return {
51
+ command: `docker run --rm --volumes-from ${target}:ro ${TAR_IMAGE} tar -czf - --numeric-owner -C ${shellQuote(mount)} . > ${file}`,
52
+ file,
53
+ notes,
54
+ };
55
+ }
56
+ /** The preview lines for every service with a data volume. */
57
+ export function renderVolumePreview(plan, receipt) {
58
+ const out = [];
59
+ for (const s of plan.services) {
60
+ const cmd = volumeCommand(s);
61
+ if (!cmd)
62
+ continue;
63
+ if (out.length === 0)
64
+ out.push('', 'Volumes (the data volume of each service; stop the service on the source first):');
65
+ const created = receipt?.items[`service:${s.ref}`];
66
+ const done = receipt?.items[`volume:${s.ref}`];
67
+ if (!created || created.state !== 'done')
68
+ out.push(` – ${s.name}: not created yet (run import apply first)`);
69
+ else if (done?.state === 'done')
70
+ out.push(` ✓ ${s.name}: imported (#${done.importId}, sha256 ${done.sha256?.slice(0, 12)}…)`);
71
+ else if ('manual' in cmd)
72
+ out.push(` – ${s.name}: ${cmd.manual}`);
73
+ else {
74
+ out.push(` + ${s.name} (${s.create.volumeMount}, NineDeploy service #${created.id}):`);
75
+ out.push(` ${cmd.command}`);
76
+ for (const n of cmd.notes)
77
+ out.push(` ${n}`);
78
+ out.push(` then: --volume ${flagArg(s.name, cmd.file)}`);
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+ /** Check every archive, then import each, recording every step in the receipt. */
84
+ export async function importVolumes(plan, archives, deps) {
85
+ const now = deps.now ?? (() => new Date());
86
+ const { receipt } = deps;
87
+ const jobs = [];
88
+ for (const [name, file] of Object.entries(archives)) {
89
+ const s = plan.services.find((x) => x.name === name);
90
+ if (!s)
91
+ throw new Error(`--volume names no service in the plan: ${name}.`);
92
+ const created = receipt.items[`service:${s.ref}`];
93
+ if (created?.state !== 'done' || created.id === undefined)
94
+ throw new Error(`${name} has not been created yet: run \`ninedeploy import apply\` first.`);
95
+ if (!s.create.volumeMount)
96
+ throw new Error(`${name} has no data volume in the plan (no volume mount), so there is nothing to import into.`);
97
+ const { sha256, bytes } = await deps.hashFile(file);
98
+ if (bytes === 0)
99
+ throw new Error(`${file} is empty.`);
100
+ const prior = receipt.items[`volume:${s.ref}`];
101
+ if (prior?.sha256 !== undefined && prior.sha256 !== sha256 && (prior.state === 'done' || prior.importId !== undefined)) {
102
+ throw new Error(`${name} already has volume import #${prior.importId} recorded for a different archive. import data does not import twice; restore its pre-import snapshot or use the panel if you mean to.`);
103
+ }
104
+ jobs.push({ s, serviceId: created.id, file, sha256, bytes });
105
+ }
106
+ for (const job of jobs) {
107
+ const key = `volume:${job.s.ref}`;
108
+ const prior = receipt.items[key];
109
+ if (prior?.state === 'done')
110
+ continue;
111
+ const base = { kind: 'volume', ref: job.s.ref, name: job.s.name, id: job.serviceId, sha256: job.sha256, bytes: job.bytes };
112
+ const put = (state, notes, importId) => {
113
+ receipt.items[key] = { ...base, state, notes, ...(importId !== undefined ? { importId } : {}), at: now().toISOString() };
114
+ deps.save(receipt);
115
+ };
116
+ let resumeImportId = prior?.sha256 === job.sha256 ? prior.importId : undefined;
117
+ put('pending', [], resumeImportId);
118
+ let row;
119
+ try {
120
+ const existing = resumeImportId === undefined ? undefined : await deps.client.volumeImports.get(job.serviceId, resumeImportId);
121
+ if (existing?.status === 'completed')
122
+ row = existing;
123
+ else if (existing?.status === 'running')
124
+ row = await deps.client.volumeImports.wait(job.serviceId, existing.id);
125
+ else {
126
+ if (existing && ['failed', 'cancelled', 'expired'].includes(existing.status)) {
127
+ resumeImportId = undefined;
128
+ put('pending', []);
129
+ }
130
+ row = await deps.client.volumeImports.importFile(job.serviceId, deps.open(job.file), {
131
+ sizeBytes: job.bytes,
132
+ sha256: job.sha256,
133
+ filename: path.basename(job.file),
134
+ ...(resumeImportId !== undefined ? { resumeImportId } : {}),
135
+ onCreated: (r) => put('pending', [], r.id),
136
+ });
137
+ row = await deps.client.volumeImports.wait(job.serviceId, row.id);
138
+ }
139
+ }
140
+ catch (err) {
141
+ const status = err.status;
142
+ const message = status === 404 && receipt.items[key]?.importId === undefined
143
+ ? 'This panel has no volume import yet: update it, or copy the files by hand.'
144
+ : err instanceof Error ? err.message : String(err);
145
+ put('failed', [message], receipt.items[key]?.importId);
146
+ continue;
147
+ }
148
+ if (row.status === 'completed') {
149
+ put('done', [
150
+ `Imported into ${row.volumeName}: ${job.bytes} bytes, sha256 checked by the panel.`,
151
+ ...(row.safetyBackupId ? [`The volume's earlier contents are in volume backup #${row.safetyBackupId} (pre-import).`] : []),
152
+ ], row.id);
153
+ }
154
+ else {
155
+ // The panel's message can quote file names from the archive, so the receipt keeps the status only.
156
+ put('failed', [`Volume import #${row.id} ended ${row.status}. Details: the service's volume imports on the panel.`], row.id);
157
+ }
158
+ }
159
+ return receipt;
160
+ }
161
+ //# sourceMappingURL=importVolumes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"importVolumes.js","sourceRoot":"","sources":["../../src/commands/importVolumes.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { activityList, alertsCreate, alertsList, alertsRemove, backupsCreate, ba
13
13
  import { notificationsCreateFcm as _notifCreateFcm, notificationsCreateWebhook as _notifCreateWebhook, notificationsList as _notifList, notificationsRemove as _notifRemove, notificationsTest as _notifTest, } from './commands/notifications.js';
14
14
  import { databasePgbouncer } from './commands/pgbouncer.js';
15
15
  import { pitrBaseBackup, pitrDisable, pitrEnable, pitrRecover, pitrRecoveries, pitrStatus } from './commands/pitr.js';
16
+ import { upgradeFinalize, upgradeRollback, upgradeStart, upgradeStatus } from './commands/databaseUpgrade.js';
16
17
  import { backupPolicyGet, backupPolicySet } from './commands/backupPolicy.js';
17
18
  import { logsSearch } from './commands/logs.js';
18
19
  import { emailTemplatesList, emailTemplatesPreview, emailTemplatesReset, emailTemplatesSet, } from './commands/emailTemplates.js';
@@ -45,6 +46,10 @@ import { logDrainQueueAction, logDrainRetryAction, logDrainDiscardAction } from
45
46
  import { buildCacheStatsAction, } from './commands/buildCache.js';
46
47
  import { brandingGetAction, brandingSetAction, } from './commands/branding.js';
47
48
  import { egressClearAction, egressListAction, egressSetAction, } from './commands/egress.js';
49
+ import { importPlanAction } from './commands/importPlan.js';
50
+ import { importApplyAction } from './commands/importApply.js';
51
+ import { importDataAction } from './commands/importData.js';
52
+ import { importCutoverAction } from './commands/importCutover.js';
48
53
  import { ipsForgetAction, ipsListAction, ipsSetAction } from './commands/hostIps.js';
49
54
  import { ssoAddAction, ssoListAction, ssoRemoveAction, } from './commands/sso.js';
50
55
  import { panelBackupDecryptAction, panelBackupListAction, panelBackupNowAction, panelBackupSetAction, panelBackupStatusAction, } from './commands/panelBackup.js';
@@ -235,6 +240,23 @@ databases
235
240
  .description('Manage the PgBouncer sidecar: enable | disable | status (postgres only)')
236
241
  .option('--port <port>', 'Override the listen port (enable only)', (v) => Number(v))
237
242
  .action((dbId, action, opts) => databasePgbouncer(getClient(), dbId, action, opts));
243
+ // ── Major-version upgrades (O2) ──────────────────────────────────────────
244
+ const upgradeCmd = databases
245
+ .command('upgrade')
246
+ .description('Move a PostgreSQL database to a newer major: dump, restore into a new volume, compare, switch (previous volume kept)');
247
+ upgradeCmd.command('status <dbId>').description('The running major, the majors it can move to, and the upgrade history')
248
+ .action((dbId) => upgradeStatus(getClient(), dbId));
249
+ upgradeCmd.command('start <dbId>').description('Upgrade to a newer major; the database stops until it is done (database admin)')
250
+ .requiredOption('--to <major>', 'Target PostgreSQL major, e.g. 18')
251
+ .option('-y, --yes', 'Skip the confirmation')
252
+ .option('--no-wait', 'Return once the upgrade is admitted')
253
+ .action((dbId, o) => upgradeStart(getClient(), dbId, o));
254
+ upgradeCmd.command('rollback <dbId> <upgradeId>').description('Point the database back at the previous volume (database admin)')
255
+ .option('-y, --yes', 'Skip the confirmation')
256
+ .action((dbId, upgradeId, o) => upgradeRollback(getClient(), dbId, upgradeId, o));
257
+ upgradeCmd.command('finalize <dbId> <upgradeId>').description('Delete the volume the database no longer uses; no rollback after this (database admin)')
258
+ .option('-y, --yes', 'Skip the confirmation')
259
+ .action((dbId, upgradeId, o) => upgradeFinalize(getClient(), dbId, upgradeId, o));
238
260
  // ── PostgreSQL point-in-time recovery (R3) ───────────────────────────────
239
261
  const pitrCmd = databases
240
262
  .command('pitr')
@@ -529,6 +551,43 @@ egressCmd
529
551
  .description('Detach the egress rule for a project')
530
552
  .option('--driver <name>', 'Target a specific driver (default: first registered)')
531
553
  .action((projectId, opts) => egressClearAction(getClient(), projectId, opts));
554
+ // ── Import from another platform (A1 step 1: read-only plan) ─────────────
555
+ const importCmd = program.command('import').description('Move from Coolify, Dokploy, CapRover or Heroku: plan (read-only), apply, data, cutover');
556
+ importCmd
557
+ .command('plan <source>')
558
+ .description('Read a coolify | dokploy | caprover panel, or a Heroku app\'s app.json and Procfile, and print what NineDeploy would create. Nothing is created or changed. The credential comes from NINEDEPLOY_IMPORT_TOKEN (Coolify, Dokploy) or NINEDEPLOY_IMPORT_PASSWORD (CapRover, plus NINEDEPLOY_IMPORT_OTP), or a hidden prompt; Heroku needs none.')
559
+ .requiredOption('--url <url>', 'The source panel (e.g. https://coolify.example.com), or for heroku the app\'s git repository')
560
+ .option('--dir <path>', 'heroku: a checkout of the repository holding app.json / Procfile (default: .)')
561
+ .option('--branch <branch>', 'heroku: the branch NineDeploy deploys (default: main)')
562
+ .option('--app <name>', 'heroku: the Heroku app name, for the pg:backups command')
563
+ .option('--json', 'Print the plan as JSON')
564
+ .option('--out <file>', 'Also write the plan (JSON, names only) to a file')
565
+ .action((source, opts) => importPlanAction(source, opts));
566
+ importCmd
567
+ .command('apply <plan>')
568
+ .description('Create the services and databases of a saved plan (import plan --out) on this panel. Without --yes it only shows what it would create. Never deploys and never adds a domain; keeps a resumable receipt next to the plan.')
569
+ .option('--yes', 'Create them')
570
+ .option('--project <id>', 'Tag every created service and database with this project')
571
+ .option('--dotenv <service=path>', 'Environment values for a service, from a .env file (repeatable)', (v, prev) => [...prev, v], [])
572
+ .option('--receipt <file>', 'Receipt file (default: <plan>.receipt.json)')
573
+ .action((plan, opts) => importApplyAction(plan, opts));
574
+ importCmd
575
+ .command('data <plan>')
576
+ .description('Move database contents into the databases import apply created. Without --yes it prints the cutover order and the dump command to run on the source host for each database.')
577
+ .option('--dump <database=file>', 'A dump to import into that database (repeatable); its sha256 is checked by the panel', (v, prev) => [...prev, v], [])
578
+ .option('--volume <service=file>', 'A gzipped tar of that service\'s data volume (repeatable); restored after a snapshot of what is there', (v, prev) => [...prev, v], [])
579
+ .option('--yes', 'Import the dumps and archives')
580
+ .option('--receipt <file>', 'Receipt file (default: <plan>.receipt.json)')
581
+ .action((plan, opts) => importDataAction(plan, opts));
582
+ importCmd
583
+ .command('cutover <plan>')
584
+ .description('Add the planned domains once each service runs a successful deploy and the hostname\'s DNS already points at this panel. Without --yes it only reports what is ready.')
585
+ .option('--yes', 'Add (or verify) the domains that pass')
586
+ .option('--expect <address>', 'The address DNS must point at (repeatable; default: Settings → DNS records)', (v, prev) => [...prev, v], [])
587
+ .option('--resolver <ip>', 'DNS server to query (default: the system\'s)')
588
+ .option('--skip-dns-check <host>', 'Skip the DNS check for a proxied host (repeatable; refused while the Cloudflare integration is on)', (v, prev) => [...prev, v], [])
589
+ .option('--receipt <file>', 'Receipt file (default: <plan>.receipt.json)')
590
+ .action((plan, opts) => importCutoverAction(plan, opts));
532
591
  // ── SSO (G-22) ───────────────────────────────────────────────────────────
533
592
  const ssoCmd = program.command('sso').description('Manage OIDC / SAML SSO providers (G-22)');
534
593
  ssoCmd.command('list').description('List every configured provider').action(() => ssoListAction(getClient()));