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:
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Workspace reader — reads session.json and deliverables from the shared workspaces PVC.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export interface SessionInfo {
|
||||
readonly workspace: string;
|
||||
readonly originalWorkflowId?: string;
|
||||
readonly webUrl?: string;
|
||||
readonly startTime?: number;
|
||||
readonly cost?: number;
|
||||
readonly resumeAttempts?: readonly { workflowId: string; timestamp: number }[];
|
||||
}
|
||||
|
||||
export function readSessionJson(workspacesDir: string, workspace: string): SessionInfo | null {
|
||||
const sessionPath = path.join(workspacesDir, workspace, 'session.json');
|
||||
try {
|
||||
const raw = fs.readFileSync(sessionPath, 'utf-8');
|
||||
const data = JSON.parse(raw) as Record<string, unknown>;
|
||||
const session = data.session as Record<string, unknown> | undefined;
|
||||
const originalWorkflowId = session?.originalWorkflowId as string | undefined;
|
||||
const webUrl = session?.webUrl as string | undefined;
|
||||
const startTime = session?.startTime as number | undefined;
|
||||
const cost = session?.totalCostUsd as number | undefined;
|
||||
const resumeAttempts = session?.resumeAttempts as SessionInfo['resumeAttempts'];
|
||||
|
||||
return {
|
||||
workspace,
|
||||
...(originalWorkflowId && { originalWorkflowId }),
|
||||
...(webUrl && { webUrl }),
|
||||
...(startTime && { startTime }),
|
||||
...(cost && { cost }),
|
||||
...(resumeAttempts && { resumeAttempts }),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function readReport(workspacesDir: string, workspace: string): string | null {
|
||||
const delivDir = path.join(workspacesDir, workspace, 'deliverables');
|
||||
try {
|
||||
const files = fs.readdirSync(delivDir);
|
||||
const reportFile = files.find((f) => f.includes('report') && f.endsWith('.md'));
|
||||
if (!reportFile) return null;
|
||||
return fs.readFileSync(path.join(delivDir, reportFile), 'utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function listWorkspaces(workspacesDir: string): SessionInfo[] {
|
||||
try {
|
||||
const entries = fs.readdirSync(workspacesDir, { withFileTypes: true });
|
||||
const results: SessionInfo[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const session = readSessionJson(workspacesDir, entry.name);
|
||||
if (session) {
|
||||
results.push(session);
|
||||
}
|
||||
}
|
||||
|
||||
return results.sort((a, b) => (b.startTime ?? 0) - (a.startTime ?? 0));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user