Add a setup helper to enable tailscale inside jails when FEATURE_TAILSCALE and an auth key are present, prefetch tailscale packages, and document the installer shortcut. --- Build: FAIL — not run Tests: FAIL — not run
263 lines
8.6 KiB
TypeScript
263 lines
8.6 KiB
TypeScript
/**
|
|
* setup/git.ts — Provision a local Bastille git jail.
|
|
*
|
|
* Creates a thin jail with git + rsync, mirrors from REMOTE_GIT_URL
|
|
* plus any GIT_MIRROR_URLS, or creates an empty bare repo as fallback.
|
|
* Fully idempotent — safe to rerun.
|
|
*/
|
|
import { execSync, spawnSync } from 'child_process';
|
|
import path from 'path';
|
|
|
|
import {
|
|
AGENT_INTERNAL_DOMAIN,
|
|
AGENT_NAME,
|
|
CODE_HOSTING_MODE,
|
|
FEATURE_GIT,
|
|
GIT_DEFAULT_REPO_NAME,
|
|
GIT_JAIL_NAME,
|
|
GIT_MIRROR_URLS,
|
|
GIT_JAIL_IP,
|
|
GIT_STORAGE_ROOT,
|
|
REMOTE_GIT_URL,
|
|
SUBNET_BASE,
|
|
} from '../src/config.js';
|
|
import { logger } from '../src/logger.js';
|
|
import { loadPackageList, mountPkgCacheInJail } from './packages.js';
|
|
import { getPlatform } from './platform.js';
|
|
import { emitStatus } from './status.js';
|
|
import { maybeEnableTailscaleInJail } from './tailscale.js';
|
|
|
|
const LOG = 'logs/setup.log';
|
|
|
|
function bastille(...args: string[]): { ok: boolean; output: string } {
|
|
const result = spawnSync('bastille', args, {
|
|
encoding: 'utf-8',
|
|
env: process.env,
|
|
});
|
|
const output = [result.stdout || '', result.stderr || '']
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
.trim();
|
|
return { ok: (result.status ?? 1) === 0, output };
|
|
}
|
|
|
|
function jailExists(name: string): boolean {
|
|
const { output } = bastille('list');
|
|
return output.split('\n').some((line) => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('JID')) {
|
|
return false;
|
|
}
|
|
const cols = trimmed.split(/\s+/u);
|
|
return cols.length > 1 && cols[1] === name;
|
|
});
|
|
}
|
|
|
|
function detectFreeBSDRelease(): string {
|
|
const output = execSync('freebsd-version -u', { encoding: 'utf-8' }).trim();
|
|
const match = output.match(/^(\d+\.\d+-\w+)/);
|
|
return match?.[1] ?? output;
|
|
}
|
|
|
|
function repoNameFromRemote(remote: string): string | null {
|
|
const trimmed = remote.trim();
|
|
if (!trimmed) return null;
|
|
const hashIndex = trimmed.indexOf('#');
|
|
const clean = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed;
|
|
|
|
try {
|
|
const url = new URL(clean);
|
|
const base = path.posix.basename(url.pathname);
|
|
return base || null;
|
|
} catch {
|
|
// Fall through to SCP-style parsing.
|
|
}
|
|
|
|
const colonIndex = clean.lastIndexOf(':');
|
|
if (colonIndex !== -1 && clean.slice(colonIndex + 1).includes('/')) {
|
|
const base = path.posix.basename(clean.slice(colonIndex + 1));
|
|
return base || null;
|
|
}
|
|
|
|
const base = path.posix.basename(clean);
|
|
return base || null;
|
|
}
|
|
|
|
function repoExistsInJail(jail: string, repoPath: string): boolean {
|
|
const { ok } = bastille('cmd', jail, 'test', '-d', repoPath);
|
|
return ok;
|
|
}
|
|
|
|
export async function run(_args: string[]): Promise<void> {
|
|
const jailName = GIT_JAIL_NAME;
|
|
const hostname = `git.${AGENT_INTERNAL_DOMAIN}`;
|
|
const repoPath = path.posix.join(GIT_STORAGE_ROOT, GIT_DEFAULT_REPO_NAME);
|
|
const extraMirrorUrls = GIT_MIRROR_URLS.filter((url) => url && url !== REMOTE_GIT_URL);
|
|
const runBastille = (args: string[]) => bastille(...args);
|
|
|
|
// Feature gate — skip if git hosting is explicitly disabled
|
|
if (!FEATURE_GIT || CODE_HOSTING_MODE === 'external') {
|
|
emitStatus('SETUP_GIT', {
|
|
STATUS: 'skipped',
|
|
REASON: !FEATURE_GIT ? 'feature_disabled' : 'external_hosting',
|
|
LOG,
|
|
});
|
|
logger.info('Git jail skipped — FEATURE_GIT=%s, CODE_HOSTING_MODE=%s', FEATURE_GIT, CODE_HOSTING_MODE);
|
|
return;
|
|
}
|
|
|
|
// Platform gate
|
|
if (getPlatform() !== 'freebsd') {
|
|
emitStatus('SETUP_GIT', {
|
|
STATUS: 'failed',
|
|
ERROR: 'unsupported_platform',
|
|
LOG,
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
const exists = jailExists(jailName);
|
|
|
|
// ── Create jail if missing ──────────────────────────────────────
|
|
if (!exists) {
|
|
const release = detectFreeBSDRelease();
|
|
const gateway = process.env.WARDEN_GATEWAY || `${SUBNET_BASE}.1`;
|
|
const bridge = process.env.WARDEN_BRIDGE || 'warden0';
|
|
|
|
logger.info({ jailName, ip: GIT_JAIL_IP, release }, 'Creating git jail');
|
|
|
|
const create = bastille(
|
|
'create', '-T', '-B',
|
|
'-g', gateway,
|
|
jailName, release, `${GIT_JAIL_IP}/24`, bridge,
|
|
);
|
|
if (!create.ok) {
|
|
throw new Error(`bastille create failed: ${create.output}`);
|
|
}
|
|
|
|
// Set hostname and restart so it takes effect
|
|
bastille('config', jailName, 'set', 'host.hostname', hostname);
|
|
bastille('restart', jailName);
|
|
} else {
|
|
logger.info({ jailName }, 'Git jail already exists, skipping creation');
|
|
}
|
|
|
|
// ── Install packages ────────────────────────────────────────────
|
|
mountPkgCacheInJail(jailName);
|
|
|
|
const packages = loadPackageList('git-jail.txt');
|
|
const pkg = bastille('pkg', jailName, 'install', '-y', ...packages);
|
|
if (!pkg.ok) {
|
|
logger.warn({ output: pkg.output }, 'Package install had warnings');
|
|
}
|
|
|
|
maybeEnableTailscaleInJail(runBastille, jailName, jailName);
|
|
|
|
// ── Create storage root ─────────────────────────────────────────
|
|
bastille('cmd', jailName, 'install', '-d', '-m', '755', GIT_STORAGE_ROOT);
|
|
|
|
// ── Create or mirror repository ─────────────────────────────────
|
|
if (!repoExistsInJail(jailName, repoPath)) {
|
|
let mirrored = false;
|
|
|
|
if (REMOTE_GIT_URL) {
|
|
logger.info({ url: REMOTE_GIT_URL }, 'Attempting mirror clone');
|
|
const clone = bastille(
|
|
'cmd', jailName,
|
|
'git', 'clone', '--mirror', REMOTE_GIT_URL, repoPath,
|
|
);
|
|
if (clone.ok) {
|
|
mirrored = true;
|
|
} else {
|
|
logger.warn(
|
|
{ output: clone.output },
|
|
'Mirror clone failed — falling back to empty bare repo',
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!mirrored) {
|
|
const init = bastille('cmd', jailName, 'git', 'init', '--bare', repoPath);
|
|
if (!init.ok) {
|
|
throw new Error(`git init --bare failed: ${init.output}`);
|
|
}
|
|
}
|
|
} else {
|
|
logger.info({ repoPath }, 'Bare repo already exists, skipping');
|
|
}
|
|
|
|
if (extraMirrorUrls.length) {
|
|
for (const mirrorUrl of extraMirrorUrls) {
|
|
const repoName = repoNameFromRemote(mirrorUrl);
|
|
if (!repoName) {
|
|
logger.warn({ mirrorUrl }, 'Unable to derive repo name for mirror URL');
|
|
continue;
|
|
}
|
|
if (repoName === GIT_DEFAULT_REPO_NAME) {
|
|
continue;
|
|
}
|
|
|
|
const mirrorRepoPath = path.posix.join(GIT_STORAGE_ROOT, repoName);
|
|
if (repoExistsInJail(jailName, mirrorRepoPath)) {
|
|
logger.info({ mirrorRepoPath }, 'Mirror repo already exists, skipping');
|
|
continue;
|
|
}
|
|
|
|
logger.info({ mirrorUrl, mirrorRepoPath }, 'Attempting mirror clone');
|
|
const clone = bastille(
|
|
'cmd', jailName,
|
|
'git', 'clone', '--mirror', mirrorUrl, mirrorRepoPath,
|
|
);
|
|
if (!clone.ok) {
|
|
logger.warn(
|
|
{ mirrorUrl, output: clone.output },
|
|
'Mirror clone failed — skipping extra repo',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Enable on boot ──────────────────────────────────────────────
|
|
try {
|
|
execSync(`sysrc jail_list+="${jailName}"`, { stdio: 'ignore' });
|
|
} catch {
|
|
logger.warn('sysrc jail_list update failed — jail may not start on boot');
|
|
}
|
|
|
|
// ── Validate ────────────────────────────────────────────────────
|
|
const gitVer = bastille('cmd', jailName, 'git', '--version');
|
|
if (!gitVer.ok) {
|
|
throw new Error('Validation failed: git not installed in jail');
|
|
}
|
|
|
|
const isBare = bastille(
|
|
'cmd', jailName,
|
|
'git', '--git-dir', repoPath, 'rev-parse', '--is-bare-repository',
|
|
);
|
|
if (!isBare.ok || !isBare.output.includes('true')) {
|
|
throw new Error(`Validation failed: ${repoPath} is not a valid bare repository`);
|
|
}
|
|
|
|
emitStatus('SETUP_GIT', {
|
|
STATUS: 'success',
|
|
JAIL_NAME: jailName,
|
|
JAIL_IP: GIT_JAIL_IP,
|
|
GIT_STORAGE_ROOT,
|
|
GIT_REPO: GIT_DEFAULT_REPO_NAME,
|
|
MIRROR_SOURCE: REMOTE_GIT_URL || 'none',
|
|
MIRROR_EXTRAS: extraMirrorUrls.join(',') || 'none',
|
|
LOG,
|
|
});
|
|
|
|
logger.info({ jailName, ip: GIT_JAIL_IP, repoPath }, 'Git jail provisioned');
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
emitStatus('SETUP_GIT', {
|
|
STATUS: 'failed',
|
|
ERROR: message,
|
|
LOG,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|