137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
/**
|
|
* Builds a kubernetes-sigs/agent-sandbox Sandbox CR manifest.
|
|
*
|
|
* The Sandbox CR creates a long-lived pod (sleep infinity entrypoint) into
|
|
* which paperclip-server can exec arbitrary commands. This solves the
|
|
* architectural mismatch with the batch/v1 Job backend, which only supports
|
|
* a single one-shot entrypoint — not the multi-command adapter-install pattern
|
|
* used by paperclip-server.
|
|
*
|
|
* Security baseline is identical to buildJobManifest (pod-spec-builder.ts):
|
|
* non-root, drop ALL caps, read-only rootFS, Tini PID 1, seccomp
|
|
* RuntimeDefault, fsGroupChangePolicy OnRootMismatch, automountSAToken=false.
|
|
*
|
|
* NOTE: paperclip-server runs OUTSIDE the cluster, so we cannot set ownerReferences
|
|
* on the Sandbox CR (the owner would need to be an in-cluster resource). The
|
|
* release path is explicit delete via sandboxCrOrchestrator.release().
|
|
*/
|
|
|
|
export interface BuildSandboxCrManifestInput {
|
|
namespace: string;
|
|
sandboxName: string;
|
|
adapterType: string;
|
|
image: string;
|
|
envSecretName: string;
|
|
serviceAccountName: string;
|
|
labels: Record<string, string>;
|
|
resources: {
|
|
requests?: { cpu?: string; memory?: string };
|
|
limits?: { cpu?: string; memory?: string };
|
|
};
|
|
runtimeClassName?: string;
|
|
imagePullSecrets?: string[];
|
|
}
|
|
|
|
export function buildSandboxCrManifest(
|
|
input: BuildSandboxCrManifestInput,
|
|
): Record<string, unknown> {
|
|
const podLabels: Record<string, string> = {
|
|
...input.labels,
|
|
"paperclip.io/role": "agent",
|
|
};
|
|
return {
|
|
apiVersion: "agents.x-k8s.io/v1alpha1",
|
|
kind: "Sandbox",
|
|
metadata: {
|
|
name: input.sandboxName,
|
|
namespace: input.namespace,
|
|
labels: { ...input.labels },
|
|
// No ownerReferences: paperclip-server is out-of-cluster. Release is
|
|
// explicit delete.
|
|
},
|
|
spec: {
|
|
podTemplate: {
|
|
metadata: {
|
|
labels: podLabels,
|
|
},
|
|
spec: {
|
|
serviceAccountName: input.serviceAccountName,
|
|
// Agent containers call back to paperclip-server via HTTPS egress;
|
|
// they never call the Kubernetes API, so mounting an SA token is
|
|
// unnecessary attack surface.
|
|
automountServiceAccountToken: false,
|
|
// Sandbox controller requires restartPolicy: Always so the pod
|
|
// stays running between exec calls.
|
|
restartPolicy: "Always",
|
|
...(input.runtimeClassName
|
|
? { runtimeClassName: input.runtimeClassName }
|
|
: {}),
|
|
...(input.imagePullSecrets && input.imagePullSecrets.length > 0
|
|
? {
|
|
imagePullSecrets: input.imagePullSecrets.map((name) => ({
|
|
name,
|
|
})),
|
|
}
|
|
: {}),
|
|
securityContext: {
|
|
runAsNonRoot: true,
|
|
runAsUser: 1000,
|
|
runAsGroup: 1000,
|
|
fsGroup: 1000,
|
|
fsGroupChangePolicy: "OnRootMismatch",
|
|
seccompProfile: { type: "RuntimeDefault" },
|
|
},
|
|
containers: [
|
|
{
|
|
name: "agent",
|
|
image: input.image,
|
|
imagePullPolicy: "IfNotPresent",
|
|
// sleep infinity keeps the pod running; paperclip-server execs
|
|
// commands into it via Kubernetes exec API. Tini as PID 1 for
|
|
// proper signal forwarding and zombie reaping.
|
|
command: [
|
|
"/usr/bin/tini",
|
|
"--",
|
|
"/bin/sh",
|
|
"-c",
|
|
"sleep infinity",
|
|
],
|
|
envFrom: [{ secretRef: { name: input.envSecretName } }],
|
|
securityContext: {
|
|
runAsNonRoot: true,
|
|
runAsUser: 1000,
|
|
runAsGroup: 1000,
|
|
readOnlyRootFilesystem: true,
|
|
allowPrivilegeEscalation: false,
|
|
capabilities: { drop: ["ALL"] },
|
|
},
|
|
resources: {
|
|
requests: input.resources.requests ?? {
|
|
cpu: "250m",
|
|
memory: "512Mi",
|
|
},
|
|
limits: input.resources.limits ?? {
|
|
cpu: "2",
|
|
memory: "4Gi",
|
|
},
|
|
},
|
|
volumeMounts: [
|
|
{ name: "workspace", mountPath: "/workspace" },
|
|
{ name: "home", mountPath: "/home/paperclip" },
|
|
{ name: "cache", mountPath: "/home/paperclip/.cache" },
|
|
{ name: "tmp", mountPath: "/tmp" },
|
|
],
|
|
},
|
|
],
|
|
volumes: [
|
|
{ name: "workspace", emptyDir: { sizeLimit: "8Gi" } },
|
|
{ name: "home", emptyDir: { sizeLimit: "1Gi" } },
|
|
{ name: "cache", emptyDir: { sizeLimit: "1Gi" } },
|
|
{ name: "tmp", emptyDir: { sizeLimit: "2Gi" } },
|
|
],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|