All files / src/preview port-allocator.ts

95% Statements 19/20
100% Branches 4/4
100% Functions 6/6
94.44% Lines 17/18

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                      9x     9x     9x                         4x 4x   4x 8x   5x 4x 4x                     5x 5x   5x 1x     5x 4x 4x          
/**
 * Port Allocator
 *
 * Finds the next available port for a preview deployment.
 * Queries running preview containers and performs a bind test.
 */
 
import { createServer } from 'node:net';
import { getPreviewPorts } from './docker-client.js';
import { createModuleLogger } from '../logger/index.js';
 
const log = createModuleLogger('preview:port');
 
/** Starting port for preview deployments */
const BASE_PORT = 9100;
 
/** Maximum port to try before giving up */
const MAX_PORT = 9200;
 
/**
 * Allocate the next available port for a preview deployment.
 *
 * 1. Queries running preview containers for their cw.port labels
 * 2. Finds the next port >= BASE_PORT that isn't in use
 * 3. Performs a bind test to verify no external conflict
 *
 * @returns An available port number
 * @throws If no port is available in the range
 */
export async function allocatePort(): Promise<number> {
  const usedPorts = new Set(await getPreviewPorts());
  log.debug({ usedPorts: Array.from(usedPorts) }, 'ports in use by previews');
 
  for (let port = BASE_PORT; port < MAX_PORT; port++) {
    if (usedPorts.has(port)) continue;
 
    if (await isPortAvailable(port)) {
      log.info({ port }, 'allocated port');
      return port;
    }
  }
 
  throw new Error(`No available ports in range ${BASE_PORT}-${MAX_PORT}`);
}
 
/**
 * Test if a port is available by attempting to bind to it.
 */
async function isPortAvailable(port: number): Promise<boolean> {
  return new Promise((resolve) => {
    const server = createServer();
 
    server.once('error', () => {
      resolve(false);
    });
 
    server.listen(port, '127.0.0.1', () => {
      server.close(() => {
        resolve(true);
      });
    });
  });
}