genbox 1.0.64 → 1.0.66
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/dist/commands/create.js +14 -294
- package/dist/commands/db-sync.js +55 -14
- package/dist/commands/rebuild.js +19 -253
- package/dist/db-utils.js +120 -0
- package/dist/utils/branch-prompt.js +231 -0
- package/dist/utils/env-parser.js +127 -0
- package/dist/utils/git-utils.js +49 -0
- package/dist/utils/index.js +24 -0
- package/dist/utils/ssh-keys.js +98 -0
- package/package.json +1 -1
package/dist/commands/rebuild.js
CHANGED
|
@@ -51,22 +51,10 @@ const api_1 = require("../api");
|
|
|
51
51
|
const genbox_selector_1 = require("../genbox-selector");
|
|
52
52
|
const schema_v4_1 = require("../schema-v4");
|
|
53
53
|
const db_utils_1 = require("../db-utils");
|
|
54
|
+
const utils_1 = require("../utils");
|
|
54
55
|
// ============================================================================
|
|
55
56
|
// SSH Utilities for Soft Rebuild
|
|
56
57
|
// ============================================================================
|
|
57
|
-
function getPrivateSshKeyPath() {
|
|
58
|
-
const home = os.homedir();
|
|
59
|
-
const potentialKeys = [
|
|
60
|
-
path.join(home, '.ssh', 'id_ed25519'),
|
|
61
|
-
path.join(home, '.ssh', 'id_rsa'),
|
|
62
|
-
];
|
|
63
|
-
for (const keyPath of potentialKeys) {
|
|
64
|
-
if (fs.existsSync(keyPath)) {
|
|
65
|
-
return keyPath;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
throw new Error('No SSH private key found in ~/.ssh/');
|
|
69
|
-
}
|
|
70
58
|
function sshExec(ip, keyPath, command, timeoutSecs = 30) {
|
|
71
59
|
const sshOpts = `-i ${keyPath} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=${timeoutSecs}`;
|
|
72
60
|
try {
|
|
@@ -389,125 +377,12 @@ async function runSoftRebuild(options) {
|
|
|
389
377
|
return { success: false, error: error.message };
|
|
390
378
|
}
|
|
391
379
|
}
|
|
392
|
-
function getPublicSshKey() {
|
|
393
|
-
const home = os.homedir();
|
|
394
|
-
const potentialKeys = [
|
|
395
|
-
path.join(home, '.ssh', 'id_ed25519.pub'),
|
|
396
|
-
path.join(home, '.ssh', 'id_rsa.pub'),
|
|
397
|
-
];
|
|
398
|
-
for (const keyPath of potentialKeys) {
|
|
399
|
-
if (fs.existsSync(keyPath)) {
|
|
400
|
-
const content = fs.readFileSync(keyPath, 'utf-8').trim();
|
|
401
|
-
if (content)
|
|
402
|
-
return content;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
throw new Error('No public SSH key found in ~/.ssh/');
|
|
406
|
-
}
|
|
407
|
-
function getPrivateSshKey() {
|
|
408
|
-
const home = os.homedir();
|
|
409
|
-
const potentialKeys = [
|
|
410
|
-
path.join(home, '.ssh', 'id_ed25519'),
|
|
411
|
-
path.join(home, '.ssh', 'id_rsa'),
|
|
412
|
-
];
|
|
413
|
-
for (const keyPath of potentialKeys) {
|
|
414
|
-
if (fs.existsSync(keyPath)) {
|
|
415
|
-
return fs.readFileSync(keyPath, 'utf-8');
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return undefined;
|
|
419
|
-
}
|
|
420
380
|
async function rebuildGenbox(id, payload) {
|
|
421
381
|
return (0, api_1.fetchApi)(`/genboxes/${id}/rebuild`, {
|
|
422
382
|
method: 'POST',
|
|
423
383
|
body: JSON.stringify(payload),
|
|
424
384
|
});
|
|
425
385
|
}
|
|
426
|
-
/**
|
|
427
|
-
* Parse .env.genbox file into segregated sections
|
|
428
|
-
*/
|
|
429
|
-
function parseEnvGenboxSections(content) {
|
|
430
|
-
const sections = new Map();
|
|
431
|
-
let currentSection = 'GLOBAL';
|
|
432
|
-
let currentContent = [];
|
|
433
|
-
for (const line of content.split('\n')) {
|
|
434
|
-
const sectionMatch = line.match(/^# === ([^=]+) ===$/);
|
|
435
|
-
if (sectionMatch) {
|
|
436
|
-
if (currentContent.length > 0) {
|
|
437
|
-
sections.set(currentSection, currentContent.join('\n').trim());
|
|
438
|
-
}
|
|
439
|
-
currentSection = sectionMatch[1].trim();
|
|
440
|
-
currentContent = [];
|
|
441
|
-
}
|
|
442
|
-
else if (currentSection !== 'END') {
|
|
443
|
-
currentContent.push(line);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
if (currentContent.length > 0 && currentSection !== 'END') {
|
|
447
|
-
sections.set(currentSection, currentContent.join('\n').trim());
|
|
448
|
-
}
|
|
449
|
-
return sections;
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Build a map of service URL variables based on connection type
|
|
453
|
-
*/
|
|
454
|
-
function buildServiceUrlMap(envVarsFromFile, connectTo) {
|
|
455
|
-
const urlMap = {};
|
|
456
|
-
const prefix = connectTo ? `${connectTo.toUpperCase()}_` : 'LOCAL_';
|
|
457
|
-
const serviceNames = new Set();
|
|
458
|
-
for (const key of Object.keys(envVarsFromFile)) {
|
|
459
|
-
const match = key.match(/^(LOCAL|STAGING|PRODUCTION)_(.+_URL)$/);
|
|
460
|
-
if (match) {
|
|
461
|
-
serviceNames.add(match[2]);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
for (const serviceName of serviceNames) {
|
|
465
|
-
const prefixedKey = `${prefix}${serviceName}`;
|
|
466
|
-
const localKey = `LOCAL_${serviceName}`;
|
|
467
|
-
const value = envVarsFromFile[prefixedKey] || envVarsFromFile[localKey];
|
|
468
|
-
if (value) {
|
|
469
|
-
urlMap[serviceName] = value;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (!urlMap['API_URL']) {
|
|
473
|
-
const apiUrl = envVarsFromFile[`${prefix}API_URL`] ||
|
|
474
|
-
envVarsFromFile['LOCAL_API_URL'] ||
|
|
475
|
-
envVarsFromFile['STAGING_API_URL'];
|
|
476
|
-
if (apiUrl) {
|
|
477
|
-
urlMap['API_URL'] = apiUrl;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return urlMap;
|
|
481
|
-
}
|
|
482
|
-
/**
|
|
483
|
-
* Build env content for a specific app
|
|
484
|
-
*/
|
|
485
|
-
function buildAppEnvContent(sections, appName, serviceUrlMap) {
|
|
486
|
-
const parts = [];
|
|
487
|
-
const globalSection = sections.get('GLOBAL');
|
|
488
|
-
if (globalSection) {
|
|
489
|
-
parts.push(globalSection);
|
|
490
|
-
}
|
|
491
|
-
const appSection = sections.get(appName);
|
|
492
|
-
if (appSection) {
|
|
493
|
-
parts.push(appSection);
|
|
494
|
-
}
|
|
495
|
-
let envContent = parts.join('\n\n');
|
|
496
|
-
for (const [varName, value] of Object.entries(serviceUrlMap)) {
|
|
497
|
-
const pattern = new RegExp(`\\$\\{${varName}\\}`, 'g');
|
|
498
|
-
envContent = envContent.replace(pattern, value);
|
|
499
|
-
}
|
|
500
|
-
envContent = envContent
|
|
501
|
-
.split('\n')
|
|
502
|
-
.filter(line => {
|
|
503
|
-
const trimmed = line.trim();
|
|
504
|
-
return trimmed === '' || trimmed.includes('=') || !trimmed.startsWith('#');
|
|
505
|
-
})
|
|
506
|
-
.join('\n')
|
|
507
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
508
|
-
.trim();
|
|
509
|
-
return envContent;
|
|
510
|
-
}
|
|
511
386
|
/**
|
|
512
387
|
* Build rebuild payload from resolved config
|
|
513
388
|
*/
|
|
@@ -527,26 +402,15 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
527
402
|
const envGenboxPath = path.join(process.cwd(), '.env.genbox');
|
|
528
403
|
if (fs.existsSync(envGenboxPath)) {
|
|
529
404
|
const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
|
|
530
|
-
const sections = parseEnvGenboxSections(rawEnvContent);
|
|
405
|
+
const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
|
|
531
406
|
const globalSection = sections.get('GLOBAL') || '';
|
|
532
|
-
const envVarsFromFile =
|
|
533
|
-
for (const line of globalSection.split('\n')) {
|
|
534
|
-
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
535
|
-
if (match) {
|
|
536
|
-
let value = match[2].trim();
|
|
537
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
538
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
539
|
-
value = value.slice(1, -1);
|
|
540
|
-
}
|
|
541
|
-
envVarsFromFile[match[1]] = value;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
407
|
+
const envVarsFromFile = (0, utils_1.parseEnvVarsFromSection)(globalSection);
|
|
544
408
|
let connectTo;
|
|
545
409
|
if (resolved.profile && config.profiles?.[resolved.profile]) {
|
|
546
410
|
const profile = config.profiles[resolved.profile];
|
|
547
411
|
connectTo = (0, config_loader_1.getProfileConnection)(profile);
|
|
548
412
|
}
|
|
549
|
-
const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
|
|
413
|
+
const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
|
|
550
414
|
if (connectTo && Object.keys(serviceUrlMap).length > 0) {
|
|
551
415
|
console.log(chalk_1.default.dim(` Using ${connectTo} URLs for variable expansion`));
|
|
552
416
|
}
|
|
@@ -558,7 +422,7 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
558
422
|
if (servicesSections.length > 0) {
|
|
559
423
|
for (const serviceSectionName of servicesSections) {
|
|
560
424
|
const serviceName = serviceSectionName.split('/')[1];
|
|
561
|
-
const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
|
|
425
|
+
const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
|
|
562
426
|
const stagingName = `${app.name}-${serviceName}.env`;
|
|
563
427
|
const targetPath = `${repoPath}/apps/${serviceName}/.env`;
|
|
564
428
|
files.push({
|
|
@@ -570,7 +434,7 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
570
434
|
}
|
|
571
435
|
}
|
|
572
436
|
else {
|
|
573
|
-
const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
|
|
437
|
+
const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
|
|
574
438
|
files.push({
|
|
575
439
|
path: `/home/dev/.env-staging/${app.name}.env`,
|
|
576
440
|
content: appEnvContent,
|
|
@@ -600,6 +464,8 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
600
464
|
sourceBranch: repo.sourceBranch,
|
|
601
465
|
};
|
|
602
466
|
}
|
|
467
|
+
// Get local git config for commits
|
|
468
|
+
const gitConfig = (0, utils_1.getGitConfig)();
|
|
603
469
|
return {
|
|
604
470
|
publicKey,
|
|
605
471
|
services,
|
|
@@ -608,110 +474,10 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
608
474
|
repos,
|
|
609
475
|
privateKey,
|
|
610
476
|
gitToken: envVars.GIT_TOKEN,
|
|
477
|
+
gitUserName: gitConfig.userName,
|
|
478
|
+
gitUserEmail: gitConfig.userEmail,
|
|
611
479
|
};
|
|
612
480
|
}
|
|
613
|
-
/**
|
|
614
|
-
* Validate git configuration and warn about missing credentials
|
|
615
|
-
*/
|
|
616
|
-
function validateGitCredentials(repos, gitToken, privateKey) {
|
|
617
|
-
const warnings = [];
|
|
618
|
-
const errors = [];
|
|
619
|
-
for (const repo of repos) {
|
|
620
|
-
const isHttps = repo.url.startsWith('https://');
|
|
621
|
-
const isSsh = repo.url.startsWith('git@') || repo.url.includes('ssh://');
|
|
622
|
-
const isPrivateRepo = repo.url.includes('github.com') && !repo.url.includes('/public/');
|
|
623
|
-
if (isHttps && !gitToken && isPrivateRepo) {
|
|
624
|
-
warnings.push(`Repository '${repo.name}' uses HTTPS URL but GIT_TOKEN is not set.`);
|
|
625
|
-
warnings.push(` Add GIT_TOKEN=<your-github-token> to .env.genbox for private repos.`);
|
|
626
|
-
}
|
|
627
|
-
if (isSsh && !privateKey) {
|
|
628
|
-
warnings.push(`Repository '${repo.name}' uses SSH URL but no SSH key was injected.`);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
return { warnings, errors };
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Prompt for branch options interactively
|
|
635
|
-
*/
|
|
636
|
-
async function promptForBranchOptions(resolved, config) {
|
|
637
|
-
// Get the default branch from config or first repo
|
|
638
|
-
const defaultBranch = config.defaults?.branch || resolved.repos[0]?.branch || 'main';
|
|
639
|
-
console.log(chalk_1.default.blue('=== Branch Configuration ==='));
|
|
640
|
-
console.log(chalk_1.default.dim(`Default branch: ${defaultBranch}`));
|
|
641
|
-
console.log('');
|
|
642
|
-
const branchChoice = await prompts.select({
|
|
643
|
-
message: 'Branch option:',
|
|
644
|
-
choices: [
|
|
645
|
-
{
|
|
646
|
-
name: `Use default branch (${defaultBranch})`,
|
|
647
|
-
value: 'default',
|
|
648
|
-
},
|
|
649
|
-
{
|
|
650
|
-
name: 'Use a different existing branch',
|
|
651
|
-
value: 'existing',
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
name: 'Create a new branch',
|
|
655
|
-
value: 'new',
|
|
656
|
-
},
|
|
657
|
-
],
|
|
658
|
-
default: 'default',
|
|
659
|
-
});
|
|
660
|
-
if (branchChoice === 'default') {
|
|
661
|
-
return resolved;
|
|
662
|
-
}
|
|
663
|
-
if (branchChoice === 'existing') {
|
|
664
|
-
const branchName = await prompts.input({
|
|
665
|
-
message: 'Enter branch name:',
|
|
666
|
-
default: defaultBranch,
|
|
667
|
-
validate: (value) => {
|
|
668
|
-
if (!value.trim())
|
|
669
|
-
return 'Branch name is required';
|
|
670
|
-
return true;
|
|
671
|
-
},
|
|
672
|
-
});
|
|
673
|
-
return {
|
|
674
|
-
...resolved,
|
|
675
|
-
repos: resolved.repos.map(repo => ({
|
|
676
|
-
...repo,
|
|
677
|
-
branch: branchName.trim(),
|
|
678
|
-
newBranch: undefined,
|
|
679
|
-
sourceBranch: undefined,
|
|
680
|
-
})),
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
if (branchChoice === 'new') {
|
|
684
|
-
const newBranchName = await prompts.input({
|
|
685
|
-
message: 'New branch name:',
|
|
686
|
-
validate: (value) => {
|
|
687
|
-
if (!value.trim())
|
|
688
|
-
return 'Branch name is required';
|
|
689
|
-
if (!/^[\w\-./]+$/.test(value))
|
|
690
|
-
return 'Invalid branch name (use letters, numbers, -, _, /, .)';
|
|
691
|
-
return true;
|
|
692
|
-
},
|
|
693
|
-
});
|
|
694
|
-
const sourceBranch = await prompts.input({
|
|
695
|
-
message: 'Create from branch:',
|
|
696
|
-
default: defaultBranch,
|
|
697
|
-
validate: (value) => {
|
|
698
|
-
if (!value.trim())
|
|
699
|
-
return 'Source branch is required';
|
|
700
|
-
return true;
|
|
701
|
-
},
|
|
702
|
-
});
|
|
703
|
-
return {
|
|
704
|
-
...resolved,
|
|
705
|
-
repos: resolved.repos.map(repo => ({
|
|
706
|
-
...repo,
|
|
707
|
-
branch: newBranchName.trim(),
|
|
708
|
-
newBranch: newBranchName.trim(),
|
|
709
|
-
sourceBranch: sourceBranch.trim(),
|
|
710
|
-
})),
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
return resolved;
|
|
714
|
-
}
|
|
715
481
|
exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
716
482
|
.description('Rebuild an existing Genbox environment with updated configuration')
|
|
717
483
|
.argument('[name]', 'Name of the Genbox to rebuild (optional - will prompt if not provided)')
|
|
@@ -822,7 +588,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
822
588
|
// Interactive branch selection only if no branch options specified and no stored branch
|
|
823
589
|
// Skip if: -b, -f, -n, stored branch, or -y
|
|
824
590
|
if (!options.branch && !options.fromBranch && !storedBranch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
|
|
825
|
-
resolved = await
|
|
591
|
+
resolved = await (0, utils_1.promptForBranchOptionsRebuild)(resolved, config);
|
|
826
592
|
}
|
|
827
593
|
// Display what will be rebuilt
|
|
828
594
|
console.log(chalk_1.default.bold('Rebuild Configuration:'));
|
|
@@ -882,7 +648,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
882
648
|
}
|
|
883
649
|
}
|
|
884
650
|
// Get SSH keys
|
|
885
|
-
const publicKey = getPublicSshKey();
|
|
651
|
+
const publicKey = (0, utils_1.getPublicSshKey)();
|
|
886
652
|
// Check if SSH auth is needed for git
|
|
887
653
|
let privateKeyContent;
|
|
888
654
|
const v3Config = config;
|
|
@@ -894,7 +660,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
894
660
|
default: true,
|
|
895
661
|
});
|
|
896
662
|
if (injectKey) {
|
|
897
|
-
privateKeyContent = getPrivateSshKey();
|
|
663
|
+
privateKeyContent = (0, utils_1.getPrivateSshKey)();
|
|
898
664
|
if (privateKeyContent) {
|
|
899
665
|
console.log(chalk_1.default.dim(' Using local SSH private key'));
|
|
900
666
|
}
|
|
@@ -1021,7 +787,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
1021
787
|
}
|
|
1022
788
|
// Validate git credentials before rebuilding
|
|
1023
789
|
const envVarsForValidation = configLoader.loadEnvVars(process.cwd());
|
|
1024
|
-
const gitValidation = validateGitCredentials(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
|
|
790
|
+
const gitValidation = (0, utils_1.validateGitCredentials)(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
|
|
1025
791
|
if (gitValidation.warnings.length > 0) {
|
|
1026
792
|
console.log('');
|
|
1027
793
|
console.log(chalk_1.default.yellow('⚠ Git Authentication Warnings:'));
|
|
@@ -1058,7 +824,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
1058
824
|
// Get SSH key path
|
|
1059
825
|
let sshKeyPath;
|
|
1060
826
|
try {
|
|
1061
|
-
sshKeyPath = getPrivateSshKeyPath();
|
|
827
|
+
sshKeyPath = (0, utils_1.getPrivateSshKeyPath)();
|
|
1062
828
|
}
|
|
1063
829
|
catch (error) {
|
|
1064
830
|
console.log(chalk_1.default.red(error.message));
|
|
@@ -1080,7 +846,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
1080
846
|
const envGenboxPath = path.join(process.cwd(), '.env.genbox');
|
|
1081
847
|
if (fs.existsSync(envGenboxPath)) {
|
|
1082
848
|
const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
|
|
1083
|
-
const sections = parseEnvGenboxSections(rawEnvContent);
|
|
849
|
+
const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
|
|
1084
850
|
const globalSection = sections.get('GLOBAL') || '';
|
|
1085
851
|
const envVarsFromFile = {};
|
|
1086
852
|
for (const line of globalSection.split('\n')) {
|
|
@@ -1099,7 +865,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
1099
865
|
const profile = config.profiles[resolved.profile];
|
|
1100
866
|
connectTo = (0, config_loader_1.getProfileConnection)(profile);
|
|
1101
867
|
}
|
|
1102
|
-
const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
|
|
868
|
+
const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
|
|
1103
869
|
for (const app of resolved.apps) {
|
|
1104
870
|
const appConfig = config.apps[app.name];
|
|
1105
871
|
const appPath = appConfig?.path || app.name;
|
|
@@ -1109,7 +875,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
1109
875
|
if (servicesSections.length > 0) {
|
|
1110
876
|
for (const serviceSectionName of servicesSections) {
|
|
1111
877
|
const serviceName = serviceSectionName.split('/')[1];
|
|
1112
|
-
const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
|
|
878
|
+
const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
|
|
1113
879
|
envFilesForSoftRebuild.push({
|
|
1114
880
|
stagingName: `${app.name}-${serviceName}.env`,
|
|
1115
881
|
remotePath: `${repoPath}/apps/${serviceName}/.env`,
|
|
@@ -1118,7 +884,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
1118
884
|
}
|
|
1119
885
|
}
|
|
1120
886
|
else {
|
|
1121
|
-
const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
|
|
887
|
+
const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
|
|
1122
888
|
envFilesForSoftRebuild.push({
|
|
1123
889
|
stagingName: `${app.name}.env`,
|
|
1124
890
|
remotePath: `${repoPath}/.env`,
|
package/dist/db-utils.js
CHANGED
|
@@ -47,9 +47,11 @@ exports.getMongoDumpInstallInstructions = getMongoDumpInstallInstructions;
|
|
|
47
47
|
exports.runLocalMongoDump = runLocalMongoDump;
|
|
48
48
|
exports.uploadDumpToGenbox = uploadDumpToGenbox;
|
|
49
49
|
exports.runRemoteMongoRestore = runRemoteMongoRestore;
|
|
50
|
+
exports.runRemoteMongoRestoreDynamic = runRemoteMongoRestoreDynamic;
|
|
50
51
|
exports.cleanupDump = cleanupDump;
|
|
51
52
|
exports.formatBytes = formatBytes;
|
|
52
53
|
exports.waitForSshAccess = waitForSshAccess;
|
|
54
|
+
exports.downloadSnapshotFromS3 = downloadSnapshotFromS3;
|
|
53
55
|
exports.uploadDumpToS3 = uploadDumpToS3;
|
|
54
56
|
exports.createAndUploadSnapshot = createAndUploadSnapshot;
|
|
55
57
|
const child_process_1 = require("child_process");
|
|
@@ -276,6 +278,94 @@ async function runRemoteMongoRestore(ipAddress, dbName, options = {}) {
|
|
|
276
278
|
});
|
|
277
279
|
});
|
|
278
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Run mongorestore on genbox via SSH with dynamic port detection
|
|
283
|
+
* This version doesn't hardcode container names or ports - it detects them dynamically
|
|
284
|
+
*/
|
|
285
|
+
async function runRemoteMongoRestoreDynamic(ipAddress, options = {}) {
|
|
286
|
+
return new Promise((resolve) => {
|
|
287
|
+
options.onProgress?.('Detecting MongoDB and restoring database...');
|
|
288
|
+
// The restore command - detects MongoDB port dynamically
|
|
289
|
+
const restoreCmd = `
|
|
290
|
+
set -e
|
|
291
|
+
|
|
292
|
+
# Detect MongoDB port from running docker containers
|
|
293
|
+
# Different projects use different port mappings (e.g., 27037, 27117, etc.)
|
|
294
|
+
MONGO_PORT=$(docker ps --format '{{.Ports}}' | grep -oP '\\d+(?=->27017)' | head -1)
|
|
295
|
+
|
|
296
|
+
if [ -z "$MONGO_PORT" ]; then
|
|
297
|
+
echo "ERROR: Could not detect MongoDB port from running containers"
|
|
298
|
+
docker ps
|
|
299
|
+
exit 1
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
echo "MongoDB detected on port: $MONGO_PORT"
|
|
303
|
+
|
|
304
|
+
# Wait for MongoDB to be responsive
|
|
305
|
+
for i in {1..30}; do
|
|
306
|
+
if mongosh --quiet --host localhost --port $MONGO_PORT --eval "db.runCommand({ping:1})" 2>/dev/null; then
|
|
307
|
+
echo "MongoDB is ready"
|
|
308
|
+
break
|
|
309
|
+
fi
|
|
310
|
+
if [ $i -eq 30 ]; then
|
|
311
|
+
echo "ERROR: MongoDB not responding after 30 attempts"
|
|
312
|
+
exit 1
|
|
313
|
+
fi
|
|
314
|
+
echo "Waiting for MongoDB... ($i/30)"
|
|
315
|
+
sleep 2
|
|
316
|
+
done
|
|
317
|
+
|
|
318
|
+
# Restore the database from the uploaded dump
|
|
319
|
+
if [ -f /home/dev/.db-dump.gz ]; then
|
|
320
|
+
echo "Restoring database..."
|
|
321
|
+
# The dump is a raw mongodump --archive file (gzipped), not a tar archive
|
|
322
|
+
# Don't use --db flag with --archive as it causes issues
|
|
323
|
+
mongorestore --host localhost:$MONGO_PORT --drop --gzip --archive=/home/dev/.db-dump.gz
|
|
324
|
+
rm -f /home/dev/.db-dump.gz
|
|
325
|
+
echo "Database restored successfully"
|
|
326
|
+
else
|
|
327
|
+
echo "Error: Dump file not found at /home/dev/.db-dump.gz"
|
|
328
|
+
exit 1
|
|
329
|
+
fi
|
|
330
|
+
`;
|
|
331
|
+
const proc = (0, child_process_1.spawn)('ssh', [
|
|
332
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
333
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
334
|
+
'-o', 'ConnectTimeout=30',
|
|
335
|
+
`dev@${ipAddress}`,
|
|
336
|
+
'bash', '-c', `'${restoreCmd.replace(/'/g, "'\\''")}'`,
|
|
337
|
+
], {
|
|
338
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
339
|
+
});
|
|
340
|
+
let stdout = '';
|
|
341
|
+
let stderr = '';
|
|
342
|
+
proc.stdout?.on('data', (data) => {
|
|
343
|
+
const line = data.toString();
|
|
344
|
+
stdout += line;
|
|
345
|
+
// Report progress
|
|
346
|
+
if (line.includes('MongoDB detected') || line.includes('Restoring') || line.includes('restored') || line.includes('Waiting')) {
|
|
347
|
+
options.onProgress?.(line.trim());
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
proc.stderr?.on('data', (data) => {
|
|
351
|
+
stderr += data.toString();
|
|
352
|
+
});
|
|
353
|
+
proc.on('close', (code) => {
|
|
354
|
+
if (code === 0) {
|
|
355
|
+
resolve({ success: true });
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
resolve({
|
|
359
|
+
success: false,
|
|
360
|
+
error: stderr || stdout || 'Restore failed',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
proc.on('error', (err) => {
|
|
365
|
+
resolve({ success: false, error: `Failed to run ssh: ${err.message}` });
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
279
369
|
/**
|
|
280
370
|
* Clean up temporary dump files
|
|
281
371
|
*/
|
|
@@ -324,6 +414,36 @@ async function waitForSshAccess(ipAddress, maxWaitSeconds = 300, onProgress) {
|
|
|
324
414
|
}
|
|
325
415
|
return false;
|
|
326
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Download a snapshot from S3 using pre-signed URL
|
|
419
|
+
*/
|
|
420
|
+
async function downloadSnapshotFromS3(downloadUrl, options = {}) {
|
|
421
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'genbox-dbdump-'));
|
|
422
|
+
const dumpPath = path.join(tempDir, 'dump.gz');
|
|
423
|
+
options.onProgress?.('Downloading snapshot from cloud...');
|
|
424
|
+
try {
|
|
425
|
+
const response = await fetch(downloadUrl);
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
428
|
+
return {
|
|
429
|
+
success: false,
|
|
430
|
+
error: `Download failed: ${response.status} ${response.statusText}`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const buffer = await response.arrayBuffer();
|
|
434
|
+
fs.writeFileSync(dumpPath, Buffer.from(buffer));
|
|
435
|
+
const stats = fs.statSync(dumpPath);
|
|
436
|
+
options.onProgress?.(`Downloaded snapshot (${formatBytes(stats.size)})`);
|
|
437
|
+
return { success: true, dumpPath };
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
441
|
+
return {
|
|
442
|
+
success: false,
|
|
443
|
+
error: `Download failed: ${error.message}`,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
327
447
|
/**
|
|
328
448
|
* Upload dump file to S3 using pre-signed URL
|
|
329
449
|
*/
|