219 lines
7.1 KiB
TypeScript
219 lines
7.1 KiB
TypeScript
import type { BuildArchitecture, BuildSpec } from "../shared/build-protocol";
|
|
|
|
export type BuildJobOptions = {
|
|
name: string;
|
|
namespace: string;
|
|
spec: BuildSpec;
|
|
workspaceClaimName: string;
|
|
workspaceSubPath?: string;
|
|
cacheImage: string;
|
|
pushImage?: string;
|
|
pushRegistryInsecure?: boolean;
|
|
cacheRegistryInsecure?: boolean;
|
|
buildkitImage?: string;
|
|
serviceAccountName?: string;
|
|
registrySecretName?: string;
|
|
labels?: Record<string, string>;
|
|
nodeSelector?: Record<string, string>;
|
|
tolerations?: Array<Record<string, unknown>>;
|
|
ttlSecondsAfterFinished?: number;
|
|
backoffLimit?: number;
|
|
};
|
|
|
|
export type KubernetesJob = {
|
|
apiVersion: "batch/v1";
|
|
kind: "Job";
|
|
metadata: {
|
|
name: string;
|
|
namespace: string;
|
|
labels: Record<string, string>;
|
|
annotations: Record<string, string>;
|
|
};
|
|
spec: Record<string, unknown>;
|
|
};
|
|
|
|
function relativeBuildPath(path: string, name: string): string {
|
|
const normalized = path === "." ? "" : path.replace(/^\.\//, "");
|
|
if (
|
|
!path ||
|
|
path.startsWith("/") ||
|
|
path.includes("\\") ||
|
|
path.includes("\0") ||
|
|
(normalized !== "" &&
|
|
normalized
|
|
.split("/")
|
|
.some((part) => !part || part === ".." || part === "."))
|
|
)
|
|
throw new Error(`${name} must be a safe workspace-relative path`);
|
|
return normalized;
|
|
}
|
|
|
|
function platform(architecture: BuildArchitecture): string {
|
|
return `linux/${architecture}`;
|
|
}
|
|
|
|
export function createBuildJob(options: BuildJobOptions): KubernetesJob {
|
|
const contextPath = relativeBuildPath(options.spec.context, "Build context");
|
|
const dockerfilePath = options.spec.dockerfile
|
|
? relativeBuildPath(options.spec.dockerfile, "Dockerfile")
|
|
: undefined;
|
|
const workspaceSubPath = options.workspaceSubPath
|
|
? relativeBuildPath(options.workspaceSubPath, "Workspace subPath")
|
|
: undefined;
|
|
if (
|
|
!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(options.name) ||
|
|
options.name.length > 63
|
|
)
|
|
throw new Error("Job name must be a valid DNS label");
|
|
|
|
const workspace = "/workspace";
|
|
const context = contextPath ? `${workspace}/${contextPath}` : workspace;
|
|
const dockerfile = dockerfilePath
|
|
? `${workspace}/${dockerfilePath}`
|
|
: `${context}/Dockerfile`;
|
|
const outputImage = options.pushImage ?? options.spec.image;
|
|
const importCacheInsecure = options.cacheRegistryInsecure
|
|
? ",registry.insecure=true"
|
|
: "";
|
|
const exportCacheInsecure = options.cacheRegistryInsecure
|
|
? ",registry.insecure=true"
|
|
: "";
|
|
const outputInsecure = options.pushRegistryInsecure
|
|
? ",registry.insecure=true"
|
|
: "";
|
|
const args = [
|
|
"build",
|
|
"--frontend=dockerfile.v0",
|
|
`--local=context=${context}`,
|
|
`--local=dockerfile=${dockerfile.slice(0, dockerfile.lastIndexOf("/"))}`,
|
|
`--opt=filename=${dockerfile.slice(dockerfile.lastIndexOf("/") + 1)}`,
|
|
`--opt=platform=${platform(options.spec.architecture)}`,
|
|
...(options.spec.target ? [`--opt=target=${options.spec.target}`] : []),
|
|
...options.spec.buildArgs.map((arg) => `--opt=build-arg:${arg}`),
|
|
`--import-cache=type=registry,ref=${options.cacheImage}${importCacheInsecure}`,
|
|
`--export-cache=type=registry,ref=${options.cacheImage},mode=max${exportCacheInsecure}`,
|
|
`--output=type=image,name=${outputImage},push=true${outputInsecure}`,
|
|
];
|
|
const labels = {
|
|
"app.kubernetes.io/name": "kuber-buildkit",
|
|
"app.kubernetes.io/managed-by": "kuber",
|
|
"kuber.astrxl.dev/build": options.name,
|
|
...options.labels,
|
|
};
|
|
|
|
return {
|
|
apiVersion: "batch/v1",
|
|
kind: "Job",
|
|
metadata: {
|
|
name: options.name,
|
|
namespace: options.namespace,
|
|
labels,
|
|
annotations: {
|
|
"container.apparmor.security.beta.kubernetes.io/buildkit": "unconfined",
|
|
"kuber.astrxl.dev/workspace": options.spec.workspace,
|
|
},
|
|
},
|
|
spec: {
|
|
backoffLimit: options.backoffLimit ?? 0,
|
|
ttlSecondsAfterFinished: options.ttlSecondsAfterFinished ?? 3600,
|
|
template: {
|
|
metadata: {
|
|
labels,
|
|
annotations: {
|
|
"container.apparmor.security.beta.kubernetes.io/buildkit":
|
|
"unconfined",
|
|
},
|
|
},
|
|
spec: {
|
|
restartPolicy: "Never",
|
|
...(options.serviceAccountName
|
|
? { serviceAccountName: options.serviceAccountName }
|
|
: {}),
|
|
automountServiceAccountToken: false,
|
|
nodeSelector: {
|
|
...options.nodeSelector,
|
|
"kubernetes.io/arch": options.spec.architecture,
|
|
},
|
|
...(options.tolerations ? { tolerations: options.tolerations } : {}),
|
|
securityContext: {
|
|
runAsNonRoot: true,
|
|
runAsUser: 1000,
|
|
runAsGroup: 1000,
|
|
fsGroup: 1000,
|
|
seccompProfile: { type: "Unconfined" },
|
|
},
|
|
...(options.registrySecretName
|
|
? { imagePullSecrets: [{ name: options.registrySecretName }] }
|
|
: {}),
|
|
containers: [
|
|
{
|
|
name: "buildkit",
|
|
image: options.buildkitImage ?? "moby/buildkit:rootless",
|
|
imagePullPolicy: "IfNotPresent",
|
|
command: ["buildctl-daemonless.sh"],
|
|
args,
|
|
env: [
|
|
{
|
|
name: "BUILDKITD_FLAGS",
|
|
value: "--oci-worker-no-process-sandbox",
|
|
},
|
|
...(options.registrySecretName
|
|
? [{ name: "DOCKER_CONFIG", value: "/docker-config" }]
|
|
: []),
|
|
],
|
|
securityContext: {
|
|
runAsNonRoot: true,
|
|
runAsUser: 1000,
|
|
allowPrivilegeEscalation: true,
|
|
seccompProfile: { type: "Unconfined" },
|
|
appArmorProfile: { type: "Unconfined" },
|
|
},
|
|
volumeMounts: [
|
|
{
|
|
name: "workspace",
|
|
mountPath: workspace,
|
|
readOnly: true,
|
|
...(workspaceSubPath ? { subPath: workspaceSubPath } : {}),
|
|
},
|
|
{
|
|
name: "buildkit-state",
|
|
mountPath: "/home/user/.local/share/buildkit",
|
|
},
|
|
...(options.registrySecretName
|
|
? [
|
|
{
|
|
name: "registry-auth",
|
|
mountPath: "/docker-config",
|
|
readOnly: true,
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
],
|
|
volumes: [
|
|
{
|
|
name: "workspace",
|
|
persistentVolumeClaim: { claimName: options.workspaceClaimName },
|
|
},
|
|
{ name: "buildkit-state", emptyDir: {} },
|
|
...(options.registrySecretName
|
|
? [
|
|
{
|
|
name: "registry-auth",
|
|
secret: {
|
|
secretName: options.registrySecretName,
|
|
items: [
|
|
{ key: ".dockerconfigjson", path: "config.json" },
|
|
],
|
|
},
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|