vibecarbon 0.5.0 → 0.6.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/README.md +1 -1
- package/carbon/.env.example +3 -0
- package/carbon/.github/workflows/deploy.yml +24 -21
- package/carbon/.github/workflows/vibecarbon-build.yml +14 -0
- package/carbon/Dockerfile +3 -0
- package/carbon/biome.json +1 -1
- package/carbon/cloud-init/k3s/master-init.sh +26 -2
- package/carbon/cloud-init/k3s/supabase-init.sh +22 -2
- package/carbon/cloud-init/k3s/worker-init.sh +22 -2
- package/carbon/content/docs/cli.mdx +1 -1
- package/carbon/docker-compose.yml +6 -0
- package/carbon/k8s/base/app/deployment.yaml +5 -0
- package/carbon/k8s/values/supabase.values.yaml +30 -0
- package/carbon/package.json +34 -36
- package/carbon/scripts/generate-sitemap.ts +18 -3
- package/carbon/src/client/App.tsx +2 -0
- package/carbon/src/client/components/NewsletterSignup.tsx +4 -3
- package/carbon/src/client/components/PricingSection.tsx +44 -1
- package/carbon/src/client/components/SEO.tsx +6 -3
- package/carbon/src/client/components/ui/calendar.tsx +1 -1
- package/carbon/src/client/index.css +9 -1
- package/carbon/src/client/index.html +3 -3
- package/carbon/src/client/locales/en.json +1 -1
- package/carbon/src/client/pages/Pricing.tsx +169 -0
- package/carbon/src/client/pages/settings/Billing.tsx +2 -6
- package/carbon/src/server/index.ts +23 -4
- package/carbon/src/shared/billing-catalog.ts +25 -0
- package/carbon/src/shared/billing-catalog.types.ts +35 -0
- package/carbon/src/shared/pricing.ts +56 -1
- package/package.json +13 -13
- package/src/activate.js +1 -1
- package/src/backup.js +33 -87
- package/src/configure.js +365 -137
- package/src/create.js +5 -2
- package/src/deploy.js +24 -7
- package/src/destroy.js +11 -4
- package/src/failover.js +64 -47
- package/src/lib/backup-format.js +84 -0
- package/src/lib/billing/stripe-catalog.js +152 -0
- package/src/lib/billing/write-catalog.js +123 -0
- package/src/lib/ci-setup.js +11 -1
- package/src/lib/config-registry.js +116 -0
- package/src/lib/deploy/compose/build-args.js +6 -0
- package/src/lib/deploy/compose/ha.js +25 -25
- package/src/lib/deploy/compose/index.js +29 -18
- package/src/lib/deploy/k8s/gitops-deploy.js +38 -14
- package/src/lib/deploy/k8s/k3s.js +20 -10
- package/src/lib/deploy/orchestrator.js +22 -1
- package/src/lib/deploy/prompts.js +21 -2
- package/src/lib/deploy/utils.js +56 -32
- package/src/lib/github-environments.js +29 -0
- package/src/lib/licensing/index.js +8 -3
- package/src/lib/licensing/tiers.js +0 -3
- package/src/lib/pod-backups.js +3 -3
- package/src/lib/providers/base.js +1 -1
- package/src/lib/providers/hetzner.js +74 -82
- package/src/lib/server-types.js +3 -31
- package/src/lib/ssh.js +19 -18
- package/src/lib/walg-backups.js +81 -0
- package/src/restore.js +122 -202
- package/src/scale.js +25 -29
- package/src/status.js +0 -69
- package/src/lib/cost.js +0 -103
package/src/backup.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { existsSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import * as p from '@clack/prompts';
|
|
16
|
+
import { formatInstant } from './lib/backup-format.js';
|
|
16
17
|
import { downloadS3Backup, listS3Backups } from './lib/backup-s3.js';
|
|
17
18
|
import { renderHelp } from './lib/cli/help.js';
|
|
18
19
|
import { parseFlags } from './lib/cli/parse-flags.js';
|
|
@@ -30,7 +31,6 @@ import { getS3Credentials } from './lib/hetzner-guided-setup.js';
|
|
|
30
31
|
import { requireLicense } from './lib/licensing/index.js';
|
|
31
32
|
import { ensureOperatorIpAccess } from './lib/operator-ip.js';
|
|
32
33
|
import { perfAsync } from './lib/perf.js';
|
|
33
|
-
import { listPodBackups } from './lib/pod-backups.js';
|
|
34
34
|
import { assertInProjectDir } from './lib/project-guard.js';
|
|
35
35
|
import {
|
|
36
36
|
getPostgresPod,
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
import { createTracker } from './lib/tracker.js';
|
|
44
44
|
import { validateBackupFilename } from './lib/validators.js';
|
|
45
45
|
import { VERSION } from './lib/version.js';
|
|
46
|
+
import { listWalgBackups } from './lib/walg-backups.js';
|
|
46
47
|
|
|
47
48
|
// ============================================================================
|
|
48
49
|
// COMMAND SPEC — single source of truth for argv parsing AND help output.
|
|
@@ -103,17 +104,21 @@ const ACTION_CHOICES = [
|
|
|
103
104
|
// ============================================================================
|
|
104
105
|
|
|
105
106
|
async function downloadPodBackup(ip, sshKeyPath, filename) {
|
|
106
|
-
const pod = getPostgresPod(ip, sshKeyPath);
|
|
107
|
+
const pod = await getPostgresPod(ip, sshKeyPath);
|
|
107
108
|
|
|
108
109
|
// Copy from pod to host
|
|
109
|
-
sshKubectl(ip, sshKeyPath, [
|
|
110
|
+
await sshKubectl(ip, sshKeyPath, [
|
|
111
|
+
'cp',
|
|
112
|
+
`vibecarbon/${pod}:/backups/${filename}`,
|
|
113
|
+
`/tmp/${filename}`,
|
|
114
|
+
]);
|
|
110
115
|
|
|
111
116
|
// SCP from host to local
|
|
112
117
|
const localPath = join(process.cwd(), filename);
|
|
113
|
-
scpDownload(ip, sshKeyPath, `/tmp/${filename}`, localPath);
|
|
118
|
+
await scpDownload(ip, sshKeyPath, `/tmp/${filename}`, localPath);
|
|
114
119
|
|
|
115
120
|
// Clean up remote temp file
|
|
116
|
-
sshRun(ip, sshKeyPath, ['rm', '-f', `/tmp/${filename}`]);
|
|
121
|
+
await sshRun(ip, sshKeyPath, ['rm', '-f', `/tmp/${filename}`]);
|
|
117
122
|
|
|
118
123
|
return localPath;
|
|
119
124
|
}
|
|
@@ -122,7 +127,7 @@ async function downloadPodBackup(ip, sshKeyPath, filename) {
|
|
|
122
127
|
// K8S JOB BACKUP (triggers CronJob-based backup)
|
|
123
128
|
// ============================================================================
|
|
124
129
|
|
|
125
|
-
function triggerBackupJob(ip, sshKeyPath) {
|
|
130
|
+
async function triggerBackupJob(ip, sshKeyPath) {
|
|
126
131
|
const timestamp = new Date()
|
|
127
132
|
.toISOString()
|
|
128
133
|
.replace(/[:.]/g, '-')
|
|
@@ -132,7 +137,7 @@ function triggerBackupJob(ip, sshKeyPath) {
|
|
|
132
137
|
const jobName = `backup-manual-${timestamp}`;
|
|
133
138
|
|
|
134
139
|
// Create a one-off Job from the CronJob template
|
|
135
|
-
sshKubectl(ip, sshKeyPath, [
|
|
140
|
+
await sshKubectl(ip, sshKeyPath, [
|
|
136
141
|
'create',
|
|
137
142
|
'job',
|
|
138
143
|
`--from=cronjob/backup`,
|
|
@@ -156,7 +161,7 @@ function triggerBackupJob(ip, sshKeyPath) {
|
|
|
156
161
|
// diagnostic returned "No pod matching" because controller-manager already
|
|
157
162
|
// GC'd the failed Pod within the ~1s window between SSH sessions).
|
|
158
163
|
try {
|
|
159
|
-
sshRunScript(
|
|
164
|
+
await sshRunScript(
|
|
160
165
|
ip,
|
|
161
166
|
sshKeyPath,
|
|
162
167
|
`export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
|
@@ -220,7 +225,7 @@ exit 1`,
|
|
|
220
225
|
// run kubectl describe for runtime context that bare logs miss.
|
|
221
226
|
let podDiag = '';
|
|
222
227
|
try {
|
|
223
|
-
podDiag = sshRunScript(
|
|
228
|
+
podDiag = await sshRunScript(
|
|
224
229
|
ip,
|
|
225
230
|
sshKeyPath,
|
|
226
231
|
`export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
|
@@ -418,7 +423,7 @@ export async function run(args) {
|
|
|
418
423
|
const s = tracker.spinner();
|
|
419
424
|
|
|
420
425
|
if (action === 'list') {
|
|
421
|
-
await runList({ s, isCompose,
|
|
426
|
+
await runList({ s, isCompose, serverIp, sshKeyPath, projectName, envName });
|
|
422
427
|
p.outro('');
|
|
423
428
|
return;
|
|
424
429
|
}
|
|
@@ -517,83 +522,23 @@ async function resolveS3Config({ isCompose, envName, projectName }) {
|
|
|
517
522
|
return s3Config;
|
|
518
523
|
}
|
|
519
524
|
|
|
520
|
-
async function runList({
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
sshKeyPath,
|
|
527
|
-
projectName,
|
|
528
|
-
envName,
|
|
529
|
-
}) {
|
|
530
|
-
s.start('Listing backups');
|
|
531
|
-
|
|
532
|
-
if (isCompose) {
|
|
533
|
-
if (useS3) {
|
|
534
|
-
try {
|
|
535
|
-
const backups = await listS3Backups(s3Config);
|
|
536
|
-
s.stop('Backups retrieved from S3');
|
|
537
|
-
printBackupList(backups, envName);
|
|
538
|
-
} catch (error) {
|
|
539
|
-
s.stop('Failed to list S3 backups');
|
|
540
|
-
p.log.error(error.message);
|
|
541
|
-
}
|
|
542
|
-
} else {
|
|
543
|
-
try {
|
|
544
|
-
const output = sshRunScript(
|
|
545
|
-
serverIp,
|
|
546
|
-
sshKeyPath,
|
|
547
|
-
`ls -lhS /opt/${projectName}/backups/*_full.tar.gz 2>/dev/null || echo NO_BACKUPS`,
|
|
548
|
-
);
|
|
549
|
-
s.stop('Backups retrieved');
|
|
550
|
-
|
|
551
|
-
if (output === 'NO_BACKUPS' || !output.trim()) {
|
|
552
|
-
p.log.info('No backups found.');
|
|
553
|
-
p.log.info(`Create one with: ${c.info(`vibecarbon backup ${envName}`)}`);
|
|
554
|
-
} else {
|
|
555
|
-
const lines = output.trim().split('\n').filter(Boolean);
|
|
556
|
-
p.log.info(`${c.bold(`Backups for ${envName}`)} (${lines.length} found):`);
|
|
557
|
-
for (const line of lines) {
|
|
558
|
-
p.log.message(` ${line}`);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
} catch (error) {
|
|
562
|
-
s.stop('Failed to list backups');
|
|
563
|
-
p.log.error(error.message);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// K8s
|
|
570
|
-
if (useS3) {
|
|
571
|
-
try {
|
|
572
|
-
const backups = await listS3Backups(s3Config);
|
|
573
|
-
s.stop('Backups retrieved from S3');
|
|
574
|
-
printBackupList(backups, envName);
|
|
575
|
-
} catch (error) {
|
|
576
|
-
s.stop('Failed to list S3 backups');
|
|
577
|
-
p.log.error(error.message);
|
|
578
|
-
process.exit(1);
|
|
579
|
-
}
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const backups = listPodBackups(serverIp, sshKeyPath, { sort: 'largest' });
|
|
584
|
-
s.stop('Backups retrieved');
|
|
585
|
-
printBackupList(backups, envName);
|
|
586
|
-
}
|
|
525
|
+
async function runList({ s, isCompose, serverIp, sshKeyPath, projectName, envName }) {
|
|
526
|
+
// List the actual wal-g base backups (the restorable ones) — NOT the legacy
|
|
527
|
+
// backups/*_full.tar.gz S3 objects, which wal-g neither writes nor restores.
|
|
528
|
+
s.start('Reading wal-g backups');
|
|
529
|
+
const backups = await listWalgBackups({ serverIp, sshKeyPath, projectName, isCompose });
|
|
530
|
+
s.stop('Backups read');
|
|
587
531
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
p.log.info('No backups found.');
|
|
532
|
+
if (backups.length === 0) {
|
|
533
|
+
p.log.info('No wal-g base backups found.');
|
|
591
534
|
p.log.info(`Create one with: ${c.info(`vibecarbon backup ${envName}`)}`);
|
|
592
535
|
return;
|
|
593
536
|
}
|
|
594
|
-
p.log.info(
|
|
595
|
-
|
|
596
|
-
|
|
537
|
+
p.log.info(
|
|
538
|
+
`${c.bold(`Backups for ${envName}`)} (${backups.length} base backup(s), newest first):`,
|
|
539
|
+
);
|
|
540
|
+
for (const b of backups) {
|
|
541
|
+
p.log.message(` ${formatInstant(b.time).padEnd(22)} ${c.dim(b.name)}`);
|
|
597
542
|
}
|
|
598
543
|
}
|
|
599
544
|
|
|
@@ -614,7 +559,7 @@ async function runDownload({
|
|
|
614
559
|
if (useS3) {
|
|
615
560
|
await downloadS3Backup(s3Config, `backups/${source}`, localPath);
|
|
616
561
|
} else {
|
|
617
|
-
scpDownload(serverIp, sshKeyPath, `/opt/${projectName}/backups/${source}`, localPath);
|
|
562
|
+
await scpDownload(serverIp, sshKeyPath, `/opt/${projectName}/backups/${source}`, localPath);
|
|
618
563
|
}
|
|
619
564
|
s.stop('Download complete');
|
|
620
565
|
p.log.success(`Saved to ${c.bold(localPath)}`);
|
|
@@ -665,7 +610,7 @@ async function runCreate({
|
|
|
665
610
|
s.start('Creating wal-g base backup...');
|
|
666
611
|
try {
|
|
667
612
|
const { backupCompose } = await import('./lib/deploy/compose/index.js');
|
|
668
|
-
backupCompose(serverIp, sshKeyPath, projectName, { retain: backupRetain });
|
|
613
|
+
await backupCompose(serverIp, sshKeyPath, projectName, { retain: backupRetain });
|
|
669
614
|
// "uploaded to S3" wording matches the k8s path (line ~689) and the e2e
|
|
670
615
|
// backup-step verification (which greps /uploaded to s3/i to confirm the
|
|
671
616
|
// backup is durable before allowing destroy → restore).
|
|
@@ -683,8 +628,9 @@ async function runCreate({
|
|
|
683
628
|
// K8s — trigger via CronJob template
|
|
684
629
|
s.start('Creating backup (triggering Kubernetes Job)');
|
|
685
630
|
try {
|
|
686
|
-
const jobName = await perfAsync(
|
|
687
|
-
|
|
631
|
+
const jobName = await perfAsync(
|
|
632
|
+
'backup.triggerJob',
|
|
633
|
+
async () => await triggerBackupJob(serverIp, sshKeyPath),
|
|
688
634
|
);
|
|
689
635
|
s.stop('Backup created');
|
|
690
636
|
|