vibecarbon 0.3.1 → 0.4.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/carbon/db/Dockerfile +14 -3
- package/carbon/docker-compose.dns01.prod.yml +42 -0
- package/carbon/k8s/base/backup/configmap-walg.yaml +47 -0
- package/carbon/k8s/base/backup/cronjob.yaml +67 -92
- package/carbon/k8s/base/backup/kustomization.yaml +1 -0
- package/carbon/k8s/base/backup/network-policy.yaml +9 -14
- package/carbon/k8s/base/backup/rbac.yaml +40 -0
- package/carbon/k8s/base/network-policies.yaml +33 -0
- package/carbon/k8s/values/supabase.values.yaml +144 -0
- package/package.json +1 -2
- package/src/backup.js +2 -36
- package/src/create.js +9 -4
- package/src/deploy.js +12 -0
- package/src/lib/backup-s3.js +0 -31
- package/src/lib/cloudflare.js +0 -59
- package/src/lib/config.js +1 -42
- package/src/lib/deploy/acme.js +57 -0
- package/src/lib/deploy/admin-user.js +105 -0
- package/src/lib/deploy/bundle.js +17 -0
- package/src/lib/deploy/compose/ha.js +94 -91
- package/src/lib/deploy/compose/index.js +50 -248
- package/src/lib/deploy/k8s/ha/index.js +12 -148
- package/src/lib/deploy/k8s/index.js +1 -21
- package/src/lib/deploy/k8s/k3s.js +314 -128
- package/src/lib/deploy/orchestrator.js +70 -35
- package/src/lib/deploy/utils.js +20 -0
- package/src/lib/hetzner-guided-setup.js +0 -7
- package/src/lib/iac/index.js +0 -9
- package/src/lib/images.js +17 -0
- package/src/lib/licensing/index.js +1 -59
- package/src/lib/licensing/tiers.js +0 -8
- package/src/lib/pod-backups.js +53 -0
- package/src/lib/providers/index.js +0 -47
- package/src/lib/ssh.js +12 -0
- package/src/restore.js +98 -265
- package/src/scale.js +43 -20
- package/carbon/backup/Dockerfile +0 -75
- package/carbon/backup/backup.sh +0 -95
- package/carbon/runtime.Dockerfile +0 -17
- package/src/lib/deploy/compose/acme-verify.js +0 -121
- package/src/lib/deploy/index.js +0 -119
- package/src/lib/kubectl.js +0 -81
|
@@ -26,7 +26,7 @@ import * as p from '@clack/prompts';
|
|
|
26
26
|
import { generateSSHKeyPair } from '../../../../deploy.js';
|
|
27
27
|
import { setupHA } from '../../../cloudflare.js';
|
|
28
28
|
import { c } from '../../../colors.js';
|
|
29
|
-
import {
|
|
29
|
+
import { runCommandAsync } from '../../../command.js';
|
|
30
30
|
import { fetchWithRetry } from '../../../fetch-retry.js';
|
|
31
31
|
import { setupHA as setupHetznerDnsHA } from '../../../hetzner-dns.js';
|
|
32
32
|
import { initBackend } from '../../../iac/index.js';
|
|
@@ -34,7 +34,7 @@ import { perfAsync } from '../../../perf.js';
|
|
|
34
34
|
import { HetznerProvider } from '../../../providers/hetzner.js';
|
|
35
35
|
import { createPrefixedTracker } from '../../../tracker.js';
|
|
36
36
|
import { StateTracker } from '../../state.js';
|
|
37
|
-
import {
|
|
37
|
+
import { readReplPassword } from '../../utils.js';
|
|
38
38
|
import { deployK3s } from '../k3s.js';
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -266,6 +266,11 @@ export async function deployK8sHA(options) {
|
|
|
266
266
|
maxWorkers: options.maxWorkers,
|
|
267
267
|
localTunnelPort: 5001,
|
|
268
268
|
perfPrefix: 'k3s.standby',
|
|
269
|
+
// DR restore seeds the PRIMARY only. The standby re-syncs from the
|
|
270
|
+
// promoted primary via replication — running wal-g backup-fetch here
|
|
271
|
+
// too would create a divergent timeline. Force off regardless of the
|
|
272
|
+
// top-level -restore flag.
|
|
273
|
+
restore: null,
|
|
269
274
|
}),
|
|
270
275
|
),
|
|
271
276
|
]);
|
|
@@ -472,137 +477,6 @@ export async function deployK8sHA(options) {
|
|
|
472
477
|
};
|
|
473
478
|
}
|
|
474
479
|
|
|
475
|
-
/**
|
|
476
|
-
* Destroy an HA deployment
|
|
477
|
-
* @param {object} options - Destruction options
|
|
478
|
-
* @returns {Promise<void>}
|
|
479
|
-
*/
|
|
480
|
-
export async function destroyK8sHA(options) {
|
|
481
|
-
const s = p.spinner();
|
|
482
|
-
|
|
483
|
-
// Destroy both clusters
|
|
484
|
-
s.start('Destroying primary cluster');
|
|
485
|
-
await destroyK8s({ ...options, environment: `${options.environment}-primary` });
|
|
486
|
-
s.stop('Primary cluster destroyed');
|
|
487
|
-
|
|
488
|
-
s.start('Destroying standby cluster');
|
|
489
|
-
await destroyK8s({ ...options, environment: `${options.environment}-standby` });
|
|
490
|
-
s.stop('Standby cluster destroyed');
|
|
491
|
-
|
|
492
|
-
p.log.success('HA deployment destroyed');
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Get HA deployment status across regions
|
|
497
|
-
* @param {object} options - Status options
|
|
498
|
-
* @returns {Promise<object>}
|
|
499
|
-
*/
|
|
500
|
-
export async function getK8sHAStatus(options = {}) {
|
|
501
|
-
const env = options.environment || 'prod';
|
|
502
|
-
const primaryKubeconfig = join(process.cwd(), '.vibecarbon', `kubeconfig-${env}-primary`);
|
|
503
|
-
const standbyKubeconfig = join(process.cwd(), '.vibecarbon', `kubeconfig-${env}-standby`);
|
|
504
|
-
|
|
505
|
-
const status = {
|
|
506
|
-
primary: { available: false },
|
|
507
|
-
standby: { available: false },
|
|
508
|
-
replication: { status: 'unknown' },
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
// Check primary
|
|
512
|
-
try {
|
|
513
|
-
const primaryPods = runCommand('kubectl get pods -n vibecarbon -o json', {
|
|
514
|
-
silent: true,
|
|
515
|
-
returnOutput: true,
|
|
516
|
-
env: { ...process.env, KUBECONFIG: primaryKubeconfig },
|
|
517
|
-
});
|
|
518
|
-
status.primary = {
|
|
519
|
-
available: true,
|
|
520
|
-
pods: JSON.parse(primaryPods),
|
|
521
|
-
};
|
|
522
|
-
} catch {
|
|
523
|
-
// Primary not available
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Check standby
|
|
527
|
-
try {
|
|
528
|
-
const standbyPods = runCommand('kubectl get pods -n vibecarbon -o json', {
|
|
529
|
-
silent: true,
|
|
530
|
-
returnOutput: true,
|
|
531
|
-
env: { ...process.env, KUBECONFIG: standbyKubeconfig },
|
|
532
|
-
});
|
|
533
|
-
status.standby = {
|
|
534
|
-
available: true,
|
|
535
|
-
pods: JSON.parse(standbyPods),
|
|
536
|
-
};
|
|
537
|
-
} catch {
|
|
538
|
-
// Standby not available
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return status;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Trigger manual failover
|
|
546
|
-
* @param {object} options - Failover options
|
|
547
|
-
* @param {string} options.targetRegion - Region to promote to primary
|
|
548
|
-
* @returns {Promise<void>}
|
|
549
|
-
*/
|
|
550
|
-
export async function triggerFailover(options) {
|
|
551
|
-
const s = p.spinner();
|
|
552
|
-
|
|
553
|
-
s.start(`Initiating failover to ${options.targetRegion}`);
|
|
554
|
-
|
|
555
|
-
// Read the activate-standby.sh script for reference
|
|
556
|
-
const activateScript = join(HA_SCRIPTS_PATH, 'activate-standby.sh');
|
|
557
|
-
if (!existsSync(activateScript)) {
|
|
558
|
-
throw new Error('Failover script not found');
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Steps from activate-standby.sh:
|
|
562
|
-
// 1. Promote PostgreSQL standby to primary
|
|
563
|
-
// 2. Verify database is writable
|
|
564
|
-
// 3. Scale up Supabase services
|
|
565
|
-
// 4. Scale up application
|
|
566
|
-
// 5. Update DNS/load balancer
|
|
567
|
-
|
|
568
|
-
const envName = options.environment || 'prod';
|
|
569
|
-
const targetKubeconfig =
|
|
570
|
-
options.targetRegion === 'standby'
|
|
571
|
-
? join(process.cwd(), '.vibecarbon', `kubeconfig-${envName}-standby`)
|
|
572
|
-
: join(process.cwd(), '.vibecarbon', `kubeconfig-${envName}-primary`);
|
|
573
|
-
|
|
574
|
-
const env = { ...process.env, KUBECONFIG: targetKubeconfig };
|
|
575
|
-
|
|
576
|
-
// Promote PostgreSQL (pg_ctl refuses to run as root — must run as postgres user)
|
|
577
|
-
runCommand(
|
|
578
|
-
'kubectl exec -n vibecarbon supabase-supabase-db-0 -- su postgres -c "pg_ctl promote -D /var/lib/postgresql/data"',
|
|
579
|
-
{ env, ignoreError: true },
|
|
580
|
-
);
|
|
581
|
-
|
|
582
|
-
// Scale up services. Supabase pods live under Helm chart Deployments
|
|
583
|
-
// named `supabase-supabase-<component>`; pre-Helm they were just
|
|
584
|
-
// `auth` / `rest` / `realtime` / `storage`. The app Deployment is our
|
|
585
|
-
// own so it stays `app`.
|
|
586
|
-
runCommand('kubectl scale deployment/app -n vibecarbon --replicas=2', { env });
|
|
587
|
-
runCommand('kubectl scale deployment/supabase-supabase-auth -n vibecarbon --replicas=2', { env });
|
|
588
|
-
runCommand('kubectl scale deployment/supabase-supabase-rest -n vibecarbon --replicas=2', { env });
|
|
589
|
-
runCommand('kubectl scale deployment/supabase-supabase-realtime -n vibecarbon --replicas=2', {
|
|
590
|
-
env,
|
|
591
|
-
});
|
|
592
|
-
runCommand('kubectl scale deployment/supabase-supabase-storage -n vibecarbon --replicas=1', {
|
|
593
|
-
env,
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
s.stop(`Failover to ${options.targetRegion} complete`);
|
|
597
|
-
|
|
598
|
-
p.log.success('Failover completed successfully');
|
|
599
|
-
p.log.info('');
|
|
600
|
-
p.log.info('Next steps:');
|
|
601
|
-
p.log.info('1. Verify application is accessible');
|
|
602
|
-
p.log.info('2. Update DNS if Cloudflare is not configured');
|
|
603
|
-
p.log.info('3. Investigate and recover the original primary');
|
|
604
|
-
}
|
|
605
|
-
|
|
606
480
|
/**
|
|
607
481
|
* Setup PostgreSQL streaming replication
|
|
608
482
|
* @param {object} options - Replication options
|
|
@@ -621,21 +495,11 @@ export async function setupReplication(options) {
|
|
|
621
495
|
const _standbySupabaseIp = options.standbySupabaseIp || standbyIp;
|
|
622
496
|
|
|
623
497
|
// replPassword is generated at create time from crypto.randomBytes and is
|
|
624
|
-
// restricted to base64url characters — no shell escaping needed.
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
// the
|
|
628
|
-
|
|
629
|
-
let replPassword = process.env.REPL_PASSWORD;
|
|
630
|
-
if (!replPassword) {
|
|
631
|
-
const envLocalPath = join(process.cwd(), '.env.local');
|
|
632
|
-
if (existsSync(envLocalPath)) {
|
|
633
|
-
const content = readFileSync(envLocalPath, 'utf-8');
|
|
634
|
-
const m =
|
|
635
|
-
content.match(/^REPL_PASSWORD="([^"]+)"/m) || content.match(/^REPL_PASSWORD='([^']+)'/m);
|
|
636
|
-
if (m) replPassword = m[1];
|
|
637
|
-
}
|
|
638
|
-
}
|
|
498
|
+
// restricted to base64url characters — no shell escaping needed. Read from
|
|
499
|
+
// process.env first (CI), then .env.local (the `vibecarbon create` default
|
|
500
|
+
// write target). Without the .env.local fallback, HA deploys fail the moment
|
|
501
|
+
// the CLI is invoked without the env var pre-exported.
|
|
502
|
+
const replPassword = readReplPassword();
|
|
639
503
|
if (!replPassword) {
|
|
640
504
|
throw new Error(
|
|
641
505
|
'REPL_PASSWORD is not set in process.env or .env.local — HA deploys require a replication password generated at create time.',
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Kubernetes Deployment Module
|
|
3
3
|
*
|
|
4
|
-
* Thin destroy
|
|
4
|
+
* Thin destroy wrapper shared by the single-cluster and HA paths.
|
|
5
5
|
* The primary deploy entry point is `deployK3s` in `./k3s.js`. All K8s
|
|
6
6
|
* modes run k3s on plain Ubuntu VMs (Hetzner cx23 by default). Callers
|
|
7
7
|
* should import it directly.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { join } from 'node:path';
|
|
11
10
|
import * as p from '@clack/prompts';
|
|
12
|
-
import { runCommand } from '../../command.js';
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
13
|
* Destroy a K8s deployment
|
|
@@ -42,21 +40,3 @@ export async function destroyK8s(options) {
|
|
|
42
40
|
throw error;
|
|
43
41
|
}
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Get K8s cluster status
|
|
48
|
-
*/
|
|
49
|
-
export async function getK8sStatus(options) {
|
|
50
|
-
const { environment } = options;
|
|
51
|
-
const kubeconfig = join(process.cwd(), '.vibecarbon', `kubeconfig-${environment}`);
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const output = runCommand(['kubectl', 'get', 'nodes', '-o', 'json'], {
|
|
55
|
-
silent: true,
|
|
56
|
-
env: { ...process.env, KUBECONFIG: kubeconfig },
|
|
57
|
-
});
|
|
58
|
-
return JSON.parse(output);
|
|
59
|
-
} catch {
|
|
60
|
-
return { items: [] };
|
|
61
|
-
}
|
|
62
|
-
}
|