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.
Files changed (42) hide show
  1. package/carbon/db/Dockerfile +14 -3
  2. package/carbon/docker-compose.dns01.prod.yml +42 -0
  3. package/carbon/k8s/base/backup/configmap-walg.yaml +47 -0
  4. package/carbon/k8s/base/backup/cronjob.yaml +67 -92
  5. package/carbon/k8s/base/backup/kustomization.yaml +1 -0
  6. package/carbon/k8s/base/backup/network-policy.yaml +9 -14
  7. package/carbon/k8s/base/backup/rbac.yaml +40 -0
  8. package/carbon/k8s/base/network-policies.yaml +33 -0
  9. package/carbon/k8s/values/supabase.values.yaml +144 -0
  10. package/package.json +1 -2
  11. package/src/backup.js +2 -36
  12. package/src/create.js +9 -4
  13. package/src/deploy.js +12 -0
  14. package/src/lib/backup-s3.js +0 -31
  15. package/src/lib/cloudflare.js +0 -59
  16. package/src/lib/config.js +1 -42
  17. package/src/lib/deploy/acme.js +57 -0
  18. package/src/lib/deploy/admin-user.js +105 -0
  19. package/src/lib/deploy/bundle.js +17 -0
  20. package/src/lib/deploy/compose/ha.js +94 -91
  21. package/src/lib/deploy/compose/index.js +50 -248
  22. package/src/lib/deploy/k8s/ha/index.js +12 -148
  23. package/src/lib/deploy/k8s/index.js +1 -21
  24. package/src/lib/deploy/k8s/k3s.js +314 -128
  25. package/src/lib/deploy/orchestrator.js +70 -35
  26. package/src/lib/deploy/utils.js +20 -0
  27. package/src/lib/hetzner-guided-setup.js +0 -7
  28. package/src/lib/iac/index.js +0 -9
  29. package/src/lib/images.js +17 -0
  30. package/src/lib/licensing/index.js +1 -59
  31. package/src/lib/licensing/tiers.js +0 -8
  32. package/src/lib/pod-backups.js +53 -0
  33. package/src/lib/providers/index.js +0 -47
  34. package/src/lib/ssh.js +12 -0
  35. package/src/restore.js +98 -265
  36. package/src/scale.js +43 -20
  37. package/carbon/backup/Dockerfile +0 -75
  38. package/carbon/backup/backup.sh +0 -95
  39. package/carbon/runtime.Dockerfile +0 -17
  40. package/src/lib/deploy/compose/acme-verify.js +0 -121
  41. package/src/lib/deploy/index.js +0 -119
  42. 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 { runCommand, runCommandAsync } from '../../../command.js';
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 { destroyK8s } from '../index.js';
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
- // Read from process.env first (CI), then .env.local (the `vibecarbon create`
626
- // default write target). Without the .env.local fallback, HA deploys fail
627
- // the moment the CLI is invoked without the env var pre-exported — which
628
- // is exactly what happened to the e2e harness in the overnight run.
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/status wrappers shared by the single-cluster and HA paths.
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
- }