feat: add K8s API server, orchestrator abstraction, and CI pipeline

- Add apps/api/ — Hono REST API server for managing pentest scans via K8s Jobs
  - POST/GET /api/scans, GET /api/scans/:id, cancel, report endpoints
  - Bearer token auth, Temporal client integration, K8s Job builder
  - Dockerfile, Kustomize manifests (Deployment, Service, RBAC)
- Add CLI orchestrator abstraction (docker.ts → Orchestrator interface)
  - DockerOrchestrator and K8sOrchestrator implementations
  - Backend detection via SHANNON_BACKEND env var or --backend flag
- Add CI workflow: type-check + lint on PR, build+push both images on main
- Switch all workflows to self-hosted runners (runners-farhoodliquor)
- Add shannon-api image build to release and release-beta workflows
- Add root infra/kustomization.yaml as Flux entry point
- Export PipelineProgress from @shannon/worker/pipeline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 13:08:51 -04:00
parent 54c92e8142
commit 1bbdd7acba
36 changed files with 2635 additions and 414 deletions
+54
View File
@@ -0,0 +1,54 @@
/**
* Backend detection — Docker (default) vs Kubernetes.
*
* Orthogonal to the local/npx mode axis. Mode controls where state lives
* and where the image comes from. Backend controls how containers are orchestrated.
*/
import type { Orchestrator } from './orchestrator.js';
export type Backend = 'docker' | 'k8s';
let cachedBackend: Backend | undefined;
let cachedOrchestrator: Orchestrator | undefined;
/**
* Detect the orchestration backend.
* SHANNON_BACKEND env var takes precedence, otherwise defaults to docker.
*/
export function getBackend(): Backend {
if (cachedBackend !== undefined) return cachedBackend;
const env = process.env.SHANNON_BACKEND;
if (env === 'k8s' || env === 'kubernetes') {
cachedBackend = 'k8s';
} else {
cachedBackend = 'docker';
}
return cachedBackend;
}
export function setBackend(backend: Backend): void {
cachedBackend = backend;
cachedOrchestrator = undefined;
}
/**
* Get the orchestrator for the current backend.
* Lazy-loads the implementation to avoid importing unused dependencies.
*/
export async function getOrchestrator(): Promise<Orchestrator> {
if (cachedOrchestrator) return cachedOrchestrator;
let orchestrator: Orchestrator;
if (getBackend() === 'k8s') {
const { K8sOrchestrator } = await import('./k8s.js');
orchestrator = new K8sOrchestrator();
} else {
const { DockerOrchestrator } = await import('./docker.js');
orchestrator = new DockerOrchestrator();
}
cachedOrchestrator = orchestrator;
return orchestrator;
}