/** * Injector manifest generation. * * The two things that must not break: a server NOT opted in produces a * byte-identical manifest to before, and a rendered secret value survives the * shell intact whatever it contains. The second is the subtle one — bad quoting * fails silently, yielding an empty token and a server that reports healthy * while every authenticated call fails. */ import { describe, it, expect } from 'vitest'; import { execFileSync } from 'node:child_process'; import { writeFileSync, mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { generatePodSpec, buildInjectorAnnotations, wrapCommandForInjector, shellSingleQuote, } from '../src/services/k8s/manifest-generator.js'; import type { ContainerSpec } from '../src/services/orchestrator.js'; const BASE: ContainerSpec = { name: 'gitea', image: 'gitea/mcp:latest' } as ContainerSpec; const CFG = { role: 'mcpctl-server-gitea', authPath: 'auth/kubernetes-worker0', mount: 'secret', pathPrefix: 'mcpctl' }; describe('generatePodSpec — opted-out servers are untouched', () => { it('emits no annotations, no serviceAccountName, automount still false', () => { const pod = generatePodSpec({ ...BASE, env: { TOKEN: 'plain' } } as ContainerSpec, 'mcpctl-servers'); expect(pod.metadata.annotations).toBeUndefined(); expect(pod.spec.serviceAccountName).toBeUndefined(); expect(pod.spec.automountServiceAccountToken).toBe(false); expect(pod.spec.containers[0]?.env).toEqual([{ name: 'TOKEN', value: 'plain' }]); }); }); describe('generatePodSpec — opted-in servers', () => { const spec = { ...BASE, annotations: buildInjectorAnnotations( [{ name: 'GITEA_ACCESS_TOKEN', secretName: 'gitea-creds', key: 'GITEA_ACCESS_TOKEN' }], CFG, ), serviceAccountName: 'mcpctl-server-gitea', automountServiceAccountToken: true, } as ContainerSpec; it('carries no secret VALUE anywhere in the manifest', () => { const pod = generatePodSpec(spec, 'mcpctl-servers'); const json = JSON.stringify(pod); expect(json).not.toContain('de4c69ed'); expect(pod.spec.containers[0]?.env ?? []).toEqual([]); }); it('runs as its own ServiceAccount with the SA token projected', () => { const pod = generatePodSpec(spec, 'mcpctl-servers'); expect(pod.spec.serviceAccountName).toBe('mcpctl-server-gitea'); // The agent cannot log in without it; false here is a silent crashloop. expect(pod.spec.automountServiceAccountToken).toBe(true); }); it('pins the auth path to the mount that validates THIS cluster', () => { const a = buildInjectorAnnotations([{ name: 'T', secretName: 's', key: 'k' }], CFG); expect(a['vault.hashicorp.com/auth-path']).toBe('auth/kubernetes-worker0'); expect(a['vault.hashicorp.com/role']).toBe('mcpctl-server-gitea'); // init container only — no sidecar in a 512Mi pod expect(a['vault.hashicorp.com/agent-pre-populate-only']).toBe('true'); }); it('groups env vars by the secret they come from', () => { const a = buildInjectorAnnotations([ { name: 'HOST', secretName: 'gitea-creds', key: 'GITEA_HOST' }, { name: 'TOKEN', secretName: 'gitea-creds', key: 'GITEA_ACCESS_TOKEN' }, { name: 'OTHER', secretName: 'other-creds', key: 'K' }, ], CFG); expect(a['vault.hashicorp.com/agent-inject-secret-gitea-creds']).toBe('secret/data/mcpctl/gitea-creds'); expect(a['vault.hashicorp.com/agent-inject-secret-other-creds']).toBe('secret/data/mcpctl/other-creds'); const tpl = a['vault.hashicorp.com/agent-inject-template-gitea-creds'] ?? ''; expect(tpl).toContain('export HOST='); expect(tpl).toContain('export TOKEN='); expect(tpl).not.toContain('export OTHER='); }); it('is stable under input reordering', () => { const refs = [ { name: 'B', secretName: 'z', key: 'k' }, { name: 'A', secretName: 'a', key: 'k' }, ]; expect(buildInjectorAnnotations(refs, CFG)).toEqual(buildInjectorAnnotations([...refs].reverse(), CFG)); }); }); describe('wrapCommandForInjector', () => { it('preserves argv exactly via $0/$@ and execs so PID 1 is the real process', () => { // exec matters: mcpd attaches to PID 1 stdin/stdout for STDIO servers. const cmd = wrapCommandForInjector(['node', 'server.js', '--port', '3000'], ['a-creds']); expect(cmd[0]).toBe('/bin/sh'); expect(cmd[1]).toBe('-c'); expect(cmd[2]).toContain('exec "$0" "$@"'); expect(cmd.slice(3)).toEqual(['node', 'server.js', '--port', '3000']); }); it('sources every referenced secret file, in a stable order', () => { const cmd = wrapCommandForInjector(['x'], ['b-creds', 'a-creds']); expect(cmd[2]).toBe('. /vault/secrets/a-creds; . /vault/secrets/b-creds; exec "$0" "$@"'); }); }); describe('shell quoting survives adversarial values', () => { // Executed by a real /bin/sh: asserting on the string alone would prove // nothing about what the shell actually does with it. const nasty = [ 'plain', 'with space', "single'quote", 'double"quote', '$USER and `whoami`', 'semi;colon && rm -rf /', 'new\nline', 'back\\slash', "'; export PWNED=1; '", ]; it.each(nasty)('round-trips %j through a sourced file', (value) => { const dir = mkdtempSync(join(tmpdir(), 'mcpctl-quote-')); const file = join(dir, 'env'); writeFileSync(file, `export SECRET=${shellSingleQuote(value)}\n`); const out = execFileSync('/bin/sh', ['-c', `. ${file}; printf '%s' "$SECRET"`], { encoding: 'utf-8' }); expect(out).toBe(value); }); it('does not let a crafted value execute anything', () => { const dir = mkdtempSync(join(tmpdir(), 'mcpctl-quote-')); const file = join(dir, 'env'); writeFileSync(file, `export SECRET=${shellSingleQuote("'; touch /tmp/mcpctl-pwned; '")}\n`); const out = execFileSync('/bin/sh', ['-c', `. ${file}; printf '%s' "$PWNED_MARKER"`], { encoding: 'utf-8' }); expect(out).toBe(''); }); }); describe('the wrapper must REPLACE the image entrypoint, not extend it', () => { // Regression: mcpd maps ContainerSpec.command -> k8s `args` so the runner // image's `npx -y` / `uvx` entrypoint still runs. Emitting the wrapper there // meant the pod actually ran `npx -y /bin/sh -c '...'`, which crashlooped. it('emits container.command (entrypoint) and no args', () => { const pod = generatePodSpec({ name: 'g', image: 'runner', entrypoint: ['/bin/sh', '-c', '. /vault/secrets/s; exec "$0" "$@"', 'npx', '-y', '@leval/mcp-grafana'], } as ContainerSpec, 'mcpctl-servers'); expect(pod.spec.containers[0]?.command?.[0]).toBe('/bin/sh'); expect(pod.spec.containers[0]?.args).toBeUndefined(); }); it('leaves the normal entrypoint+args path alone when not wrapping', () => { const pod = generatePodSpec({ name: 'g', image: 'runner', command: ['@leval/mcp-grafana'] } as ContainerSpec, 'mcpctl-servers'); expect(pod.spec.containers[0]?.command).toBeUndefined(); expect(pod.spec.containers[0]?.args).toEqual(['@leval/mcp-grafana']); }); });