All files / services teleportBotAgent.ts

100% Statements 61/61
86.66% Branches 13/15
100% Functions 3/3
100% Lines 57/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 16512x 12x 12x 12x 12x     12x 12x                                                                                         12x               6x 6x 6x 6x     6x 6x   6x 6x 2x 2x   6x 6x   6x 6x   6x 2x 2x 2x       2x     6x             6x 5x     6x   6x 6x   6x 6x   6x 6x 6x 6x 6x       6x 6x 6x 6x 6x 6x 6x       12x       1x                         2x 2x   2x 2x             1x   1x 1x 1x        
import { spawn, ChildProcess } from 'child_process';
import fs from 'fs';
import * as process from 'node:process';
import os from 'os';
import path from 'path';
import { Command } from 'commander';
// Ensure process.kill is configurable for testing by defining own property
try {
  Object.defineProperty(process, 'kill', {
    configurable: true,
    writable: true,
    value: process.kill,
  });
} catch {
  // ignore
}
 
 
/** Options for launching a Teleport application bot (tbot) */
export interface TeleportBotAgentOptions {
  /** Name of the Teleport application */
  appName: string;
  /** Join token for the bot (required on first start, optional afterwards) */
  joinToken?: string;
  /** Working directory where 'data' and 'dest' folders will be created */
  workDir: string;
  /** TTL of short-lived machine certificates (e.g. '8h') */
  certificateTtl?: string;
  /** Interval at which short-lived certificates are renewed (must be < TTL, e.g. '1h') */
  renewalInterval?: string;
}
 
/** Result of launching the Teleport bot */
export interface TeleportBotAgentResult {
  /** PID of the launched tbot process */
  pid: number;
  /** Application name used */
  appName: string;
  /** Proxy server used */
  proxyServer: string;
  /** Path to generated certificate file */
  certPath: string;
  /** Path to generated key file */
  keyPath: string;
  /** Path to tbot log file */
  logPath: string;
}
 
/**
 * Launches a Teleport application bot (tbot) in background (detached),
 * creating 'data' and 'dest' subdirectories under the provided workDir,
 * and streaming stdout/stderr to console.
 */
export async function teleportBotAgent({
  appName,
  joinToken,
  workDir,
  certificateTtl = '8h',
  renewalInterval = '1h',
}: TeleportBotAgentOptions): Promise<TeleportBotAgentResult> {
  // Ensure workDir exists and add .gitignore to ignore contents
  fs.mkdirSync(workDir, { recursive: true });
  const gitignorePath = path.join(workDir, '.gitignore');
  if (!fs.existsSync(gitignorePath)) {
    fs.writeFileSync(gitignorePath, '*\n!.gitignore\n');
  }
  // Prepare data and dest subdirectories
  const dataDir = path.join(workDir, 'data');
  fs.mkdirSync(dataDir, { recursive: true });
  // If stale lock file exists in dataDir, warn and remove it
  const lockFile = path.join(dataDir, 'lock');
  if (fs.existsSync(lockFile)) {
    console.warn(`Warning: removing stale lock file at ${lockFile}`);
    try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
  }
  const destDir = path.join(workDir, 'dest');
  fs.mkdirSync(destDir, { recursive: true });
 
  const pidFile = path.join(workDir, 'tbot.pid');
  const proxyServer = 'teleport.ftprod.fr:443';
  // If a previous tbot run exists, kill its process and remove PID
  if (fs.existsSync(pidFile)) {
    try {
      const oldPid = parseInt(fs.readFileSync(pidFile, 'utf8'), 10);
      if (!isNaN(oldPid)) process.kill(oldPid);
    } catch {
      // ignore errors
    }
    try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
  }
  // Build tbot start arguments
  const args: string[] = [
    'start',
    'application',
    `--storage=file://${dataDir}`,
    `--destination=file://${destDir}`,
    `--app=${appName}`,
  ];
  if (joinToken) {
    args.push(`--token=${joinToken}`);
  }
  // join-method is always required
  args.push('--join-method=token', `--proxy-server=${proxyServer}`, '--specific-tls-extensions');
  // Optional certificate TTL and renewal interval
  if (certificateTtl) {
    args.push(`--certificate-ttl=${certificateTtl}`);
  }
  if (renewalInterval) {
    args.push(`--renewal-interval=${renewalInterval}`);
  }
  console.log(`Launching tbot command: tbot ${args.join(' ')}`);
  const logPath = path.join(workDir, 'tbot.log');
  const outFd = fs.openSync(logPath, 'a');
  const errFd = fs.openSync(logPath, 'a');
  const bot: ChildProcess = spawn('tbot', args, {
    detached: true,
    stdio: ['ignore', outFd, errFd],
  });
  fs.closeSync(outFd);
  fs.closeSync(errFd);
  bot.unref();
  try { fs.writeFileSync(pidFile, `${bot.pid}`); } catch (_err) {}
  const certPath = path.join(destDir, 'tls.crt');
  const keyPath = path.join(destDir, 'tls.key');
  return { pid: bot.pid ?? -1, appName, proxyServer, certPath, keyPath, logPath };
}
 
/** CLI registration for teleportBotAgent */
export const cli = {
  command: 'start-app',
  description: 'Lance un Teleport application bot en arrière-plan',
  builder: (cmd: Command) =>
    cmd
      .requiredOption('--app <name>', 'Nom de l’application Teleport')
      .option('--token <token>', 'Join token pour le bot')
      .option('--workDir <path>', "Répertoire de travail pour 'data' et 'dest'")
      .option('--certificate-ttl <ttl>', 'TTL of short-lived machine certificates (e.g. 8h)', '8h')
      .option('--renewal-interval <interval>', 'Interval at which certificates are renewed (must be < TTL, e.g. 1h)', '1h'),
  handler: async (opts: {
    app: string;
    token?: string;
    workDir?: string;
    certificateTtl?: string;
    renewalInterval?: string;
  }) => {
    try {
      const workDir = opts.workDir ?? os.homedir() + '/.ftprod-ai/tbot';
      // eslint-disable-next-line @typescript-eslint/no-var-requires
      const { teleportBotAgent: run } = require('./teleportBotAgent');
      const res = await run({
        appName: opts.app,
        joinToken: opts.token,
        workDir,
        certificateTtl: opts.certificateTtl,
        renewalInterval: opts.renewalInterval,
      });
      console.log('Bot lancé:', JSON.stringify(res));
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : String(err);
      console.error('Erreur lancement tbot:', msg);
      process.exit(1);
    }
  },
};