7b16bf98f7
Renames API server, worker jobs, credentials secret, and workspaces PVC to use the hightower prefix. Upstream Shannon names (namespace, Temporal service, package imports, .shannon/ dir) are unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
142 lines
4.2 KiB
TypeScript
142 lines
4.2 KiB
TypeScript
/**
|
|
* K8s Job spec builder for worker scan Jobs.
|
|
* Constructs a Job that runs the Shannon worker image with the correct
|
|
* volumes, env, and security context. Optionally includes a git clone init container.
|
|
*/
|
|
|
|
import type * as k8s from '@kubernetes/client-node';
|
|
|
|
export interface JobParams {
|
|
readonly jobName: string;
|
|
readonly namespace: string;
|
|
readonly workerImage: string;
|
|
readonly targetUrl: string;
|
|
readonly taskQueue: string;
|
|
readonly workspace: string;
|
|
readonly credentialsSecretName: string;
|
|
readonly gitUrl?: string;
|
|
readonly gitRef?: string;
|
|
readonly repoPath?: string;
|
|
readonly configYaml?: string;
|
|
readonly pipelineTesting?: boolean;
|
|
}
|
|
|
|
const WORKER_LABEL = 'hightower-worker';
|
|
const REPO_MOUNT_PATH = '/repo';
|
|
|
|
export function buildJobSpec(params: JobParams): k8s.V1Job {
|
|
const repoPath = params.repoPath ?? REPO_MOUNT_PATH;
|
|
|
|
// 1. Build worker command
|
|
const command = ['node', 'apps/worker/dist/temporal/worker.js', params.targetUrl, repoPath];
|
|
const args: string[] = ['--task-queue', params.taskQueue, '--workspace', params.workspace];
|
|
if (params.pipelineTesting) {
|
|
args.push('--pipeline-testing');
|
|
}
|
|
|
|
// 2. Build volumes and mounts
|
|
const volumes: k8s.V1Volume[] = [
|
|
{ name: 'workspaces', persistentVolumeClaim: { claimName: 'hightower-workspaces' } },
|
|
{ name: 'shm', emptyDir: { medium: 'Memory', sizeLimit: '2Gi' } },
|
|
];
|
|
|
|
const volumeMounts: k8s.V1VolumeMount[] = [
|
|
{ name: 'workspaces', mountPath: '/app/workspaces' },
|
|
{ name: 'shm', mountPath: '/dev/shm' },
|
|
];
|
|
|
|
// Overlay dirs (writable areas over the read-only repo)
|
|
for (const overlay of ['deliverables', 'scratchpad', 'playwright-cli']) {
|
|
const volName = `overlay-${overlay}`;
|
|
volumes.push({ name: volName, emptyDir: {} });
|
|
volumeMounts.push({
|
|
name: volName,
|
|
mountPath: `${repoPath}/.shannon/${overlay === 'playwright-cli' ? '.playwright-cli' : overlay}`,
|
|
});
|
|
}
|
|
|
|
// 3. Repo volume — emptyDir for git clone, or PVC sub-path for pre-staged repos
|
|
const initContainers: k8s.V1Container[] = [];
|
|
|
|
if (params.gitUrl) {
|
|
// Git clone into an emptyDir
|
|
volumes.push({ name: 'repo', emptyDir: {} });
|
|
volumeMounts.push({ name: 'repo', mountPath: REPO_MOUNT_PATH, readOnly: true });
|
|
|
|
const cloneArgs = ['clone', '--depth', '1'];
|
|
if (params.gitRef) {
|
|
cloneArgs.push('--branch', params.gitRef);
|
|
}
|
|
cloneArgs.push(params.gitUrl, REPO_MOUNT_PATH);
|
|
|
|
initContainers.push({
|
|
name: 'git-clone',
|
|
image: 'bitnami/git:2',
|
|
command: ['git'],
|
|
args: cloneArgs,
|
|
volumeMounts: [{ name: 'repo', mountPath: REPO_MOUNT_PATH }],
|
|
});
|
|
} else if (params.repoPath) {
|
|
// Repo already on a PVC — mount the workspaces PVC (assumes repo is staged there)
|
|
volumeMounts.push({
|
|
name: 'workspaces',
|
|
mountPath: repoPath,
|
|
readOnly: true,
|
|
subPath: `repos/${params.workspace}`,
|
|
});
|
|
}
|
|
|
|
// 4. Env vars
|
|
const env: k8s.V1EnvVar[] = [{ name: 'TEMPORAL_ADDRESS', value: 'shannon-temporal:7233' }];
|
|
|
|
// 5. Construct the Job
|
|
return {
|
|
apiVersion: 'batch/v1',
|
|
kind: 'Job',
|
|
metadata: {
|
|
name: params.jobName,
|
|
namespace: params.namespace,
|
|
labels: {
|
|
app: WORKER_LABEL,
|
|
'shannon.io/workspace': params.workspace,
|
|
'shannon.io/scan-id': params.jobName,
|
|
},
|
|
},
|
|
spec: {
|
|
backoffLimit: 0,
|
|
ttlSecondsAfterFinished: 3600,
|
|
template: {
|
|
metadata: {
|
|
labels: {
|
|
app: WORKER_LABEL,
|
|
'shannon.io/workspace': params.workspace,
|
|
},
|
|
},
|
|
spec: {
|
|
restartPolicy: 'Never',
|
|
serviceAccountName: 'default',
|
|
securityContext: {
|
|
seccompProfile: { type: 'Unconfined' },
|
|
},
|
|
...(initContainers.length > 0 && { initContainers }),
|
|
containers: [
|
|
{
|
|
name: 'worker',
|
|
image: params.workerImage,
|
|
command,
|
|
args,
|
|
env,
|
|
envFrom: [{ secretRef: { name: params.credentialsSecretName } }],
|
|
volumeMounts,
|
|
resources: {
|
|
requests: { memory: '2Gi' },
|
|
},
|
|
},
|
|
],
|
|
volumes,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|