vibecarbon 0.3.0 → 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 +3 -3
- 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
package/carbon/backup/backup.sh
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -e
|
|
3
|
-
# pipefail is critical: the pg_dump | gzip pipeline below silently uploaded
|
|
4
|
-
# 170-byte gzip-header-only tarballs when pg_dump failed (wrong PGHOST),
|
|
5
|
-
# making restore think backups existed when they were empty. Observed
|
|
6
|
-
# 2026-04-27 k8s-ha matrix #6 — restore failed "No backups found in S3"
|
|
7
|
-
# because the broken tarball had no SQL inside.
|
|
8
|
-
set -o pipefail
|
|
9
|
-
|
|
10
|
-
# ============================================================================
|
|
11
|
-
# Vibecarbon Blazing Fast Backup Script (WAL-G)
|
|
12
|
-
#
|
|
13
|
-
# Optimized for performance and robustness using continuous archiving.
|
|
14
|
-
# Fallback to pg_dump for legacy compatibility.
|
|
15
|
-
#
|
|
16
|
-
# Environment Variables:
|
|
17
|
-
# PGHOST, PGPORT, PGUSER, PGDATABASE, PGPASSWORD - PostgreSQL connection
|
|
18
|
-
# S3_BUCKET, S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_REGION - S3 config
|
|
19
|
-
# S3_BACKUP_BUCKET - Dedicated backup bucket (falls back to S3_BUCKET)
|
|
20
|
-
# BACKUP_RETENTION_DAYS - Days to keep backups (default: 7)
|
|
21
|
-
# PROJECT_NAME - Project name for backup naming
|
|
22
|
-
# BACKUP_MODE - "walg" (default) or "pg_dump"
|
|
23
|
-
# ============================================================================
|
|
24
|
-
|
|
25
|
-
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
26
|
-
PROJECT="${PROJECT_NAME:-vibecarbon}"
|
|
27
|
-
MODE="${BACKUP_MODE:-walg}"
|
|
28
|
-
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
|
|
29
|
-
|
|
30
|
-
echo "=== Vibecarbon Backup (${MODE}) ==="
|
|
31
|
-
echo "Started at $(date)"
|
|
32
|
-
|
|
33
|
-
# --- S3 CONFIGURATION ---
|
|
34
|
-
UPLOAD_BUCKET="${S3_BACKUP_BUCKET:-${S3_BUCKET}}"
|
|
35
|
-
export AWS_ACCESS_KEY_ID="${S3_ACCESS_KEY}"
|
|
36
|
-
export AWS_SECRET_ACCESS_KEY="${S3_SECRET_KEY}"
|
|
37
|
-
export AWS_DEFAULT_REGION="${S3_REGION:-us-east-1}"
|
|
38
|
-
export AWS_ENDPOINT="${S3_ENDPOINT}"
|
|
39
|
-
|
|
40
|
-
if [ -z "${UPLOAD_BUCKET}" ] || [ -z "${AWS_ACCESS_KEY_ID}" ] || [ -z "${AWS_SECRET_ACCESS_KEY}" ] || [ -z "${AWS_ENDPOINT}" ]; then
|
|
41
|
-
echo "Error: S3 configuration missing. Backup cannot proceed."
|
|
42
|
-
exit 1
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
# --- WAL-G CONFIGURATION ---
|
|
46
|
-
export WALG_S3_PREFIX="s3://${UPLOAD_BUCKET}/backups/${PROJECT}/walg"
|
|
47
|
-
# Use the same credentials for WAL-G (it uses AWS_* by default, but we can be explicit)
|
|
48
|
-
export WALE_S3_PREFIX="${WALG_S3_PREFIX}"
|
|
49
|
-
# Parallelism for faster compression/upload
|
|
50
|
-
export WALG_COMPRESSION_METHOD="lz4"
|
|
51
|
-
export WALG_UPLOAD_CONCURRENCY=4
|
|
52
|
-
|
|
53
|
-
if [ "${MODE}" = "walg" ]; then
|
|
54
|
-
echo "Creating full base backup with WAL-G..."
|
|
55
|
-
# WAL-G requires PGHOST to be reachable.
|
|
56
|
-
# If running in a sidecar, it might need to connect via socket or network.
|
|
57
|
-
# We assume PGHOST is set correctly (e.g. "db").
|
|
58
|
-
export PGDATA="/var/lib/postgresql/data"
|
|
59
|
-
|
|
60
|
-
# Check if we can run backup-push.
|
|
61
|
-
# Note: backup-push usually needs to run on the database server or have access to PGDATA
|
|
62
|
-
# if it's doing a direct filesystem scan.
|
|
63
|
-
# If this script runs in a separate container, it uses the network protocol.
|
|
64
|
-
|
|
65
|
-
wal-g backup-push "${PGDATA}"
|
|
66
|
-
|
|
67
|
-
echo "Full base backup completed."
|
|
68
|
-
|
|
69
|
-
echo "Cleaning up old backups (retention: ${RETENTION_DAYS} days)..."
|
|
70
|
-
wal-g delete before "$(date -d "-${RETENTION_DAYS} days" +%Y-%m-%dT%H:%M:%SZ)" --confirm
|
|
71
|
-
|
|
72
|
-
else
|
|
73
|
-
# --- LEGACY PG_DUMP FALLBACK ---
|
|
74
|
-
echo "Falling back to pg_dump..."
|
|
75
|
-
WORK_DIR="/tmp/backup_${TIMESTAMP}"
|
|
76
|
-
mkdir -p "${WORK_DIR}"
|
|
77
|
-
ARCHIVE_NAME="${PROJECT}_${TIMESTAMP}_full.tar.gz"
|
|
78
|
-
|
|
79
|
-
PGPASSWORD="${PGPASSWORD}" pg_dump \
|
|
80
|
-
-h "${PGHOST:-postgres}" \
|
|
81
|
-
-p "${PGPORT:-5432}" \
|
|
82
|
-
-U "${PGUSER:-supabase_admin}" \
|
|
83
|
-
"${PGDATABASE:-postgres}" | gzip > "${WORK_DIR}/postgres.sql.gz"
|
|
84
|
-
|
|
85
|
-
tar czf "/tmp/${ARCHIVE_NAME}" -C "${WORK_DIR}" .
|
|
86
|
-
|
|
87
|
-
aws s3 cp "/tmp/${ARCHIVE_NAME}" \
|
|
88
|
-
"s3://${UPLOAD_BUCKET}/backups/${ARCHIVE_NAME}" \
|
|
89
|
-
--endpoint-url "${S3_ENDPOINT}"
|
|
90
|
-
|
|
91
|
-
rm -rf "${WORK_DIR}"
|
|
92
|
-
rm "/tmp/${ARCHIVE_NAME}"
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
echo "=== Backup completed at $(date) ==="
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# syntax=docker/dockerfile:1
|
|
2
|
-
# Vibecarbon Runtime Base Image
|
|
3
|
-
# This image contains the pre-installed node_modules for the Vibecarbon template.
|
|
4
|
-
# Using this as a base for your app image reduces build times from minutes to seconds.
|
|
5
|
-
|
|
6
|
-
FROM node:22-alpine3.23 AS base
|
|
7
|
-
|
|
8
|
-
RUN npm install -g pnpm@9
|
|
9
|
-
WORKDIR /app
|
|
10
|
-
|
|
11
|
-
# Pre-install dependencies
|
|
12
|
-
COPY package.json pnpm-lock.yaml ./
|
|
13
|
-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
|
|
14
|
-
pnpm install --frozen-lockfile
|
|
15
|
-
|
|
16
|
-
# This image is meant to be a base, so we stop here.
|
|
17
|
-
# The app build will continue FROM this image.
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verify Traefik has acquired Let's Encrypt certificates after compose-up,
|
|
3
|
-
* and self-heal a stuck ACME state by restarting Traefik.
|
|
4
|
-
*
|
|
5
|
-
* Why this exists: Traefik's ACME engine bursts ~7 attempts in ~30s on
|
|
6
|
-
* container start, then waits ~24h for the next renewal-check tick. If
|
|
7
|
-
* the first burst fails (typically because DNS hasn't propagated yet —
|
|
8
|
-
* see src/lib/dns-propagation.js for the cold-deploy fix), Traefik
|
|
9
|
-
* settles into a state where acme.json has 0 issued certs and the
|
|
10
|
-
* customer's browser sees the TRAEFIK DEFAULT CERT
|
|
11
|
-
* (NET::ERR_CERT_AUTHORITY_INVALID).
|
|
12
|
-
*
|
|
13
|
-
* Cold deploys on v3.6.2+ avoid the original race by writing DNS before
|
|
14
|
-
* compose-up. But customers stuck on v3.6.1's failure state can't recover
|
|
15
|
-
* via warm redeploy alone — `docker compose up -d` is a no-op for
|
|
16
|
-
* Traefik when nothing changed, so the broken in-memory state persists.
|
|
17
|
-
*
|
|
18
|
-
* This helper polls acme.json post-deploy expecting at least one cert;
|
|
19
|
-
* if Traefik is still empty after ~90s, it restarts the container,
|
|
20
|
-
* which forces a fresh ACME run against now-valid DNS. Healthy DNS
|
|
21
|
-
* yields a cert in ~30s of the restart. If the restart still doesn't
|
|
22
|
-
* produce a cert, we log a warning but don't fail the deploy — could
|
|
23
|
-
* be LE rate limits, port 80 unreachable, etc., none of which a CLI
|
|
24
|
-
* restart can fix.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { sshRunAsync } from './index.js';
|
|
28
|
-
|
|
29
|
-
const POLL_INTERVAL_MS = 5_000;
|
|
30
|
-
const INITIAL_POLL_TIMEOUT_MS = 90_000;
|
|
31
|
-
const POST_RESTART_POLL_TIMEOUT_MS = 90_000;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* SSH into the server, exec `cat /letsencrypt/acme.json` inside the
|
|
35
|
-
* Traefik container, return the count of issued certs (0 if anything
|
|
36
|
-
* goes wrong — empty acme.json, malformed JSON, SSH error, etc.).
|
|
37
|
-
*/
|
|
38
|
-
async function readAcmeCertCount(ip, sshKeyPath, projectName) {
|
|
39
|
-
let raw;
|
|
40
|
-
try {
|
|
41
|
-
raw = await sshRunAsync(
|
|
42
|
-
ip,
|
|
43
|
-
sshKeyPath,
|
|
44
|
-
`cd /opt/${projectName} && docker compose exec -T traefik cat /letsencrypt/acme.json 2>/dev/null`,
|
|
45
|
-
{ timeout: 15_000 },
|
|
46
|
-
);
|
|
47
|
-
} catch {
|
|
48
|
-
return 0;
|
|
49
|
-
}
|
|
50
|
-
if (!raw || typeof raw !== 'string') return 0;
|
|
51
|
-
try {
|
|
52
|
-
const parsed = JSON.parse(raw);
|
|
53
|
-
const certs = parsed?.letsencrypt?.Certificates;
|
|
54
|
-
return Array.isArray(certs) ? certs.length : 0;
|
|
55
|
-
} catch {
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function pollForCert(ip, sshKeyPath, projectName, timeoutMs) {
|
|
61
|
-
const deadline = Date.now() + timeoutMs;
|
|
62
|
-
while (Date.now() < deadline) {
|
|
63
|
-
if ((await readAcmeCertCount(ip, sshKeyPath, projectName)) > 0) return true;
|
|
64
|
-
const remaining = deadline - Date.now();
|
|
65
|
-
if (remaining <= 0) break;
|
|
66
|
-
await new Promise((r) => setTimeout(r, Math.min(POLL_INTERVAL_MS, remaining)));
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Wait for Traefik to acquire at least one cert; if it doesn't within
|
|
73
|
-
* the initial budget, restart the container and wait again. Logs progress
|
|
74
|
-
* via the provided logger (typically @clack/prompts p.log). Never throws
|
|
75
|
-
* — TLS issuance is best-effort; the deploy succeeds even if the cert
|
|
76
|
-
* never lands (operator can investigate via SSH).
|
|
77
|
-
*
|
|
78
|
-
* @param {string} ip - server IP
|
|
79
|
-
* @param {string} sshKeyPath - path to private key
|
|
80
|
-
* @param {string} projectName - compose project name (= directory in /opt/)
|
|
81
|
-
* @param {string} domain - configured domain (for log messages only)
|
|
82
|
-
* @param {{ message?: Function, success?: Function, warn?: Function }} log
|
|
83
|
-
*/
|
|
84
|
-
export async function ensureTraefikCert(ip, sshKeyPath, projectName, domain, log) {
|
|
85
|
-
log.message?.(`Verifying TLS certificate for ${domain}...`);
|
|
86
|
-
const acquired = await pollForCert(ip, sshKeyPath, projectName, INITIAL_POLL_TIMEOUT_MS);
|
|
87
|
-
if (acquired) {
|
|
88
|
-
log.success?.(`TLS certificate issued for ${domain}`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
log.warn?.(
|
|
92
|
-
`Traefik has no certificate for ${domain} after ${INITIAL_POLL_TIMEOUT_MS / 1000}s — ` +
|
|
93
|
-
`restarting to clear stale ACME state and retry`,
|
|
94
|
-
);
|
|
95
|
-
try {
|
|
96
|
-
await sshRunAsync(ip, sshKeyPath, `cd /opt/${projectName} && docker compose restart traefik`, {
|
|
97
|
-
timeout: 60_000,
|
|
98
|
-
});
|
|
99
|
-
} catch (err) {
|
|
100
|
-
log.warn?.(`Traefik restart failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const acquiredAfterRestart = await pollForCert(
|
|
104
|
-
ip,
|
|
105
|
-
sshKeyPath,
|
|
106
|
-
projectName,
|
|
107
|
-
POST_RESTART_POLL_TIMEOUT_MS,
|
|
108
|
-
);
|
|
109
|
-
if (acquiredAfterRestart) {
|
|
110
|
-
log.success?.(`TLS certificate issued for ${domain} after Traefik restart`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
log.warn?.(
|
|
114
|
-
`Traefik still has no certificate for ${domain} after restart. ` +
|
|
115
|
-
`Check DNS resolution (dig +short ${domain}) and acme.json on the server. ` +
|
|
116
|
-
`If both look correct, ACME may complete in the background within minutes.`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Re-exported for tests; not part of the public surface.
|
|
121
|
-
export const __test__ = { readAcmeCertCount, pollForCert };
|
package/src/lib/deploy/index.js
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deployment Module Registry
|
|
3
|
-
*
|
|
4
|
-
* Four deployment modes:
|
|
5
|
-
* - compose: Docker Compose on single VPS (Fast + Pro)
|
|
6
|
-
* - compose-ha: Docker Compose HA on 2 VPS with PostgreSQL replication (Fast + Pro)
|
|
7
|
-
* - standard: Kubernetes single-region (Pro only)
|
|
8
|
-
* - ha: Kubernetes multi-region HA (Pro only)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
deployComposeHA,
|
|
13
|
-
destroyComposeHA,
|
|
14
|
-
getComposeHAStatus,
|
|
15
|
-
} from './compose/ha.js';
|
|
16
|
-
export {
|
|
17
|
-
backupCompose,
|
|
18
|
-
deployCompose,
|
|
19
|
-
destroyCompose,
|
|
20
|
-
getComposeStatus,
|
|
21
|
-
redeployCompose,
|
|
22
|
-
restoreCompose,
|
|
23
|
-
setupServer,
|
|
24
|
-
setupServerFiles,
|
|
25
|
-
waitForSSH as waitForComposeSSH,
|
|
26
|
-
} from './compose/index.js';
|
|
27
|
-
export { deployK8sHA, destroyK8sHA, getK8sHAStatus, triggerFailover } from './k8s/ha/index.js';
|
|
28
|
-
export { destroyK8s, getK8sStatus } from './k8s/index.js';
|
|
29
|
-
export { deployK3s } from './k8s/k3s.js';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Get the appropriate deploy function based on mode
|
|
33
|
-
* @param {string} mode - 'compose', 'compose-ha', 'kubernetes', or 'ha'
|
|
34
|
-
* @returns {Promise<Function>} Deploy function
|
|
35
|
-
*/
|
|
36
|
-
export function getDeployFunction(mode = 'compose') {
|
|
37
|
-
if (mode === 'ha') {
|
|
38
|
-
return import('./k8s/ha/index.js').then((m) => m.deployK8sHA);
|
|
39
|
-
}
|
|
40
|
-
if (mode === 'compose-ha') {
|
|
41
|
-
return import('./compose/ha.js').then((m) => m.deployComposeHA);
|
|
42
|
-
}
|
|
43
|
-
if (mode === 'kubernetes') {
|
|
44
|
-
return import('./k8s/k3s.js').then((m) => m.deployK3s);
|
|
45
|
-
}
|
|
46
|
-
return import('./compose/index.js').then((m) => m.deployCompose);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get the appropriate destroy function based on mode
|
|
51
|
-
* @param {string} mode - 'compose', 'compose-ha', 'kubernetes', or 'ha'
|
|
52
|
-
* @returns {Promise<Function>} Destroy function
|
|
53
|
-
*/
|
|
54
|
-
export function getDestroyFunction(mode = 'compose') {
|
|
55
|
-
if (mode === 'ha') {
|
|
56
|
-
return import('./k8s/ha/index.js').then((m) => m.destroyK8sHA);
|
|
57
|
-
}
|
|
58
|
-
if (mode === 'compose-ha') {
|
|
59
|
-
return import('./compose/ha.js').then((m) => m.destroyComposeHA);
|
|
60
|
-
}
|
|
61
|
-
if (mode === 'kubernetes') {
|
|
62
|
-
return import('./k8s/index.js').then((m) => m.destroyK8s);
|
|
63
|
-
}
|
|
64
|
-
return import('./compose/index.js').then((m) => m.destroyCompose);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Deployment mode information
|
|
69
|
-
*/
|
|
70
|
-
export const MODES = {
|
|
71
|
-
compose: {
|
|
72
|
-
name: 'Compose',
|
|
73
|
-
displayName: 'Docker Compose',
|
|
74
|
-
description: 'Single-server deployment with Docker Compose',
|
|
75
|
-
tier: 'diamond',
|
|
76
|
-
features: [
|
|
77
|
-
'Docker Compose on single VPS',
|
|
78
|
-
'Auto HTTPS via Traefik/Caddy',
|
|
79
|
-
'All add-ons supported',
|
|
80
|
-
'Direct image transfer (no registry needed)',
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
'compose-ha': {
|
|
84
|
-
name: 'Compose HA',
|
|
85
|
-
displayName: 'Docker Compose HA',
|
|
86
|
-
description: 'Multi-region Docker Compose with PostgreSQL replication',
|
|
87
|
-
tier: 'diamond',
|
|
88
|
-
features: [
|
|
89
|
-
'Docker Compose on 2 VPS',
|
|
90
|
-
'PostgreSQL streaming replication',
|
|
91
|
-
'Cloudflare health checks + failover',
|
|
92
|
-
'Auto HTTPS via Traefik',
|
|
93
|
-
],
|
|
94
|
-
},
|
|
95
|
-
standard: {
|
|
96
|
-
name: 'Standard',
|
|
97
|
-
displayName: 'Kubernetes',
|
|
98
|
-
description: 'Single-region Kubernetes cluster',
|
|
99
|
-
tier: 'fullerene',
|
|
100
|
-
features: [
|
|
101
|
-
'Kubernetes cluster (1 master + workers)',
|
|
102
|
-
'Horizontal Pod Autoscaling',
|
|
103
|
-
'Traefik ingress with auto-SSL',
|
|
104
|
-
'Local Docker build & push',
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
ha: {
|
|
108
|
-
name: 'HA',
|
|
109
|
-
displayName: 'Kubernetes HA',
|
|
110
|
-
description: 'Multi-region Kubernetes with high availability',
|
|
111
|
-
tier: 'fullerene',
|
|
112
|
-
features: [
|
|
113
|
-
'Multi-region clusters',
|
|
114
|
-
'PostgreSQL streaming replication',
|
|
115
|
-
'One-command failover',
|
|
116
|
-
'DNS health checks',
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
};
|
package/src/lib/kubectl.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Kubectl helpers that avoid exposing secrets in argv.
|
|
3
|
-
*
|
|
4
|
-
* Any Kubernetes manifest containing secret data MUST use
|
|
5
|
-
* kubectlApplyManifest so the bytes flow through stdin instead of the
|
|
6
|
-
* shell command line.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { spawnSync } from 'node:child_process';
|
|
10
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
-
import { tmpdir } from 'node:os';
|
|
12
|
-
import { join } from 'node:path';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Apply a Kubernetes manifest via `kubectl apply -f -` piped from stdin.
|
|
16
|
-
* The manifest body (including any secret data) never appears in argv.
|
|
17
|
-
*
|
|
18
|
-
* @param {object} manifest - Kubernetes resource as a plain object
|
|
19
|
-
* @param {object} [options]
|
|
20
|
-
* @param {string} [options.namespace]
|
|
21
|
-
* @param {object} [options.env]
|
|
22
|
-
* @param {number} [options.timeout=60000]
|
|
23
|
-
* @returns {string} - kubectl stdout
|
|
24
|
-
*/
|
|
25
|
-
export function kubectlApplyManifest(manifest, options = {}) {
|
|
26
|
-
const yaml = JSON.stringify(manifest);
|
|
27
|
-
const args = ['apply', '-f', '-'];
|
|
28
|
-
if (options.namespace) args.push('-n', options.namespace);
|
|
29
|
-
const res = spawnSync('kubectl', args, {
|
|
30
|
-
input: yaml,
|
|
31
|
-
encoding: 'utf-8',
|
|
32
|
-
env: options.env ?? process.env,
|
|
33
|
-
timeout: options.timeout ?? 60_000,
|
|
34
|
-
});
|
|
35
|
-
if (res.status !== 0) {
|
|
36
|
-
const stderr = (res.stderr || '').trim();
|
|
37
|
-
const exitCode = res.status ?? '(null)';
|
|
38
|
-
// NEVER include res.stdout — kubectl may echo the submitted manifest on
|
|
39
|
-
// failure, and the manifest may contain secret data.
|
|
40
|
-
throw new Error(`kubectl apply failed (exit ${exitCode}): ${stderr || '(no stderr)'}`);
|
|
41
|
-
}
|
|
42
|
-
return res.stdout;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Apply a strategic-merge patch to a resource via a temp patch file.
|
|
47
|
-
* Use this instead of interpolating JSON into a shell command line.
|
|
48
|
-
*
|
|
49
|
-
* @param {string} resourceRef - e.g. 'deployment/app' or 'serviceaccount/default'
|
|
50
|
-
* @param {string} namespace
|
|
51
|
-
* @param {object} patch - patch object
|
|
52
|
-
* @param {object} [options]
|
|
53
|
-
* @param {string} [options.type='strategic'] - 'strategic' | 'merge' | 'json'
|
|
54
|
-
* @param {object} [options.env]
|
|
55
|
-
* @param {boolean} [options.ignoreError]
|
|
56
|
-
* @param {number} [options.timeout=60000]
|
|
57
|
-
* @returns {string|null} - kubectl stdout, or null when ignoreError + failure
|
|
58
|
-
*/
|
|
59
|
-
export function kubectlPatch(resourceRef, namespace, patch, options = {}) {
|
|
60
|
-
const { type = 'strategic', env, ignoreError = false, timeout = 60_000 } = options;
|
|
61
|
-
const dir = mkdtempSync(join(tmpdir(), 'vibecarbon-patch-'));
|
|
62
|
-
const patchFile = join(dir, 'patch.json');
|
|
63
|
-
try {
|
|
64
|
-
writeFileSync(patchFile, JSON.stringify(patch), { mode: 0o600 });
|
|
65
|
-
const args = ['patch', resourceRef, '-n', namespace, '--type', type, '--patch-file', patchFile];
|
|
66
|
-
const res = spawnSync('kubectl', args, {
|
|
67
|
-
encoding: 'utf-8',
|
|
68
|
-
env: env ?? process.env,
|
|
69
|
-
timeout,
|
|
70
|
-
});
|
|
71
|
-
if (res.status !== 0) {
|
|
72
|
-
if (ignoreError) return null;
|
|
73
|
-
const stderr = (res.stderr || '').trim();
|
|
74
|
-
const exitCode = res.status ?? '(null)';
|
|
75
|
-
throw new Error(`kubectl patch failed (exit ${exitCode}): ${stderr || '(no stderr)'}`);
|
|
76
|
-
}
|
|
77
|
-
return res.stdout;
|
|
78
|
-
} finally {
|
|
79
|
-
rmSync(dir, { recursive: true, force: true });
|
|
80
|
-
}
|
|
81
|
-
}
|