feat: release 2.7.0-rc2
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
import type { BuildJobOptions, KubernetesJob } from "./build-job";
|
||||
|
||||
// The Heroku 26 builder is multi-architecture and ships lifecycle 0.21.22.
|
||||
export const DEFAULT_BUILDPACKS_IMAGE =
|
||||
"docker.io/heroku/builder:26@sha256:d71287feea697567159584a0e8fa9c1af8d8419b07829bf40dd793cc26c7f7ad";
|
||||
|
||||
export function validateBuildpacksImage(image: string): void {
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9._:/-]*@sha256:[a-f0-9]{64}$/.test(image))
|
||||
throw new Error("Buildpacks builder image must be digest-pinned (sha256)");
|
||||
}
|
||||
|
||||
function safePath(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 registry(image: string): string {
|
||||
const first = image.split("/")[0]!;
|
||||
return first.includes(".") || first.includes(":") || first === "localhost"
|
||||
? first
|
||||
: "docker.io";
|
||||
}
|
||||
|
||||
export function createBuildpacksJob(
|
||||
options: BuildJobOptions & {
|
||||
buildpacksImage?: string;
|
||||
buildpackSubPath?: string;
|
||||
},
|
||||
): KubernetesJob {
|
||||
const builderImage = options.buildpacksImage ?? DEFAULT_BUILDPACKS_IMAGE;
|
||||
validateBuildpacksImage(builderImage);
|
||||
if (options.spec.builder !== "buildpacks")
|
||||
throw new Error("Buildpacks Job requires the buildpacks builder");
|
||||
if (
|
||||
options.spec.dockerfile !== undefined ||
|
||||
options.spec.target !== undefined ||
|
||||
options.spec.buildArgs.length
|
||||
)
|
||||
throw new Error(
|
||||
"Buildpacks does not support dockerfile, target, or buildArgs",
|
||||
);
|
||||
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 context = safePath(options.spec.context, "Build context");
|
||||
const subPath = options.workspaceSubPath
|
||||
? safePath(options.workspaceSubPath, "Workspace subPath")
|
||||
: undefined;
|
||||
if (!!options.spec.buildpackUri !== !!options.buildpackSubPath)
|
||||
throw new Error("Custom buildpack URI requires a staged package path");
|
||||
const packagePath = options.buildpackSubPath
|
||||
? safePath(options.buildpackSubPath, "Buildpack subPath")
|
||||
: undefined;
|
||||
const packageMounts = packagePath
|
||||
? [
|
||||
{
|
||||
name: "workspace",
|
||||
mountPath: "/package",
|
||||
subPath: packagePath,
|
||||
readOnly: true,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const customMounts = packagePath
|
||||
? [
|
||||
{
|
||||
name: "custom-buildpacks",
|
||||
mountPath: "/custom-buildpacks",
|
||||
readOnly: true,
|
||||
},
|
||||
...packageMounts,
|
||||
]
|
||||
: [];
|
||||
const images = [
|
||||
options.pushImage ?? options.spec.image,
|
||||
...(options.pushImages ?? []),
|
||||
];
|
||||
if (images.some((image) => !image || /[,"\r\n\s]/.test(image)))
|
||||
throw new Error("Invalid Buildpacks output image name");
|
||||
// The lifecycle's registry exporter rejects tags spanning registries.
|
||||
if (new Set(images.map(registry)).size > 1)
|
||||
throw new Error("Buildpacks output images must use the same registry");
|
||||
if (!options.cacheImage || /[,"\r\n\s]/.test(options.cacheImage))
|
||||
throw new Error("Invalid Buildpacks cache image name");
|
||||
const insecureRegistries = new Set<string>();
|
||||
if (options.pushRegistryInsecure)
|
||||
images.forEach((image) => {
|
||||
insecureRegistries.add(registry(image));
|
||||
});
|
||||
if (options.cacheRegistryInsecure)
|
||||
insecureRegistries.add(registry(options.cacheImage));
|
||||
const labels = {
|
||||
"app.kubernetes.io/name": "kuber-buildpacks",
|
||||
"app.kubernetes.io/managed-by": "kuber",
|
||||
"kuber.astrxl.dev/build": options.name,
|
||||
...options.labels,
|
||||
};
|
||||
const tolerations = [...(options.tolerations ?? [])];
|
||||
const amd64Toleration = {
|
||||
key: "arch",
|
||||
operator: "Equal",
|
||||
value: "amd64",
|
||||
effect: "NoExecute",
|
||||
};
|
||||
if (
|
||||
options.spec.architecture === "amd64" &&
|
||||
!tolerations.some(
|
||||
(value) =>
|
||||
Object.keys(amd64Toleration).length === Object.keys(value).length &&
|
||||
Object.entries(amd64Toleration).every(
|
||||
([key, entry]) => value[key] === entry,
|
||||
),
|
||||
)
|
||||
)
|
||||
tolerations.push(amd64Toleration);
|
||||
const env = [
|
||||
// lifecycle 0.21.22 requires this input; 0.15 enables --insecure-registry.
|
||||
{ name: "CNB_PLATFORM_API", value: "0.15" },
|
||||
{ name: "TMPDIR", value: "/tmp" },
|
||||
...(options.registrySecretName
|
||||
? [{ name: "DOCKER_CONFIG", value: "/docker-config" }]
|
||||
: []),
|
||||
];
|
||||
const writableMounts = [
|
||||
{ name: "app", mountPath: "/cnb-app" },
|
||||
{ name: "layers", mountPath: "/layers" },
|
||||
{ name: "cache", mountPath: "/cache" },
|
||||
{ name: "platform", mountPath: "/platform" },
|
||||
{ name: "tmp", mountPath: "/tmp" },
|
||||
];
|
||||
const authMounts = options.registrySecretName
|
||||
? [{ name: "registry-auth", mountPath: "/docker-config", readOnly: true }]
|
||||
: [];
|
||||
return {
|
||||
apiVersion: "batch/v1",
|
||||
kind: "Job",
|
||||
metadata: {
|
||||
name: options.name,
|
||||
namespace: options.namespace,
|
||||
labels,
|
||||
annotations: { "kuber.astrxl.dev/workspace": options.spec.workspace },
|
||||
},
|
||||
spec: {
|
||||
backoffLimit: options.backoffLimit ?? 0,
|
||||
ttlSecondsAfterFinished: options.ttlSecondsAfterFinished ?? 3600,
|
||||
template: {
|
||||
metadata: { labels },
|
||||
spec: {
|
||||
restartPolicy: "Never",
|
||||
...(options.serviceAccountName
|
||||
? { serviceAccountName: options.serviceAccountName }
|
||||
: {}),
|
||||
automountServiceAccountToken: false,
|
||||
nodeSelector: {
|
||||
...options.nodeSelector,
|
||||
"kubernetes.io/arch": options.spec.architecture,
|
||||
},
|
||||
...(tolerations.length ? { tolerations } : {}),
|
||||
securityContext: {
|
||||
runAsNonRoot: true,
|
||||
runAsUser: 1000,
|
||||
runAsGroup: 1000,
|
||||
fsGroup: 1000,
|
||||
seccompProfile: { type: "RuntimeDefault" },
|
||||
},
|
||||
...(options.registrySecretName
|
||||
? { imagePullSecrets: [{ name: options.registrySecretName }] }
|
||||
: {}),
|
||||
initContainers: [
|
||||
...(packagePath
|
||||
? [
|
||||
{
|
||||
name: "prepare-buildpacks",
|
||||
image: builderImage,
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
command: ["/bin/sh", "-ec"],
|
||||
args: [
|
||||
'cp -R /cnb/buildpacks/. /custom-buildpacks/; for directory in /package/buildpacks/*; do rm -rf "/custom-buildpacks/${directory##*/}"; done; cp -R /package/buildpacks/. /custom-buildpacks/',
|
||||
],
|
||||
securityContext: {
|
||||
allowPrivilegeEscalation: false,
|
||||
runAsNonRoot: true,
|
||||
runAsUser: 1000,
|
||||
runAsGroup: 1000,
|
||||
},
|
||||
volumeMounts: [
|
||||
...packageMounts,
|
||||
{
|
||||
name: "custom-buildpacks",
|
||||
mountPath: "/custom-buildpacks",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "copy-source",
|
||||
image: builderImage,
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
command: ["/bin/sh", "-ec"],
|
||||
args: [
|
||||
'cp -R "$1/." /cnb-app/ && chmod -R u+rwX /cnb-app',
|
||||
"--",
|
||||
context ? `/workspace/${context}` : "/workspace",
|
||||
],
|
||||
securityContext: {
|
||||
allowPrivilegeEscalation: false,
|
||||
runAsNonRoot: true,
|
||||
runAsUser: 1000,
|
||||
runAsGroup: 1000,
|
||||
},
|
||||
volumeMounts: [
|
||||
{
|
||||
name: "workspace",
|
||||
mountPath: "/workspace",
|
||||
readOnly: true,
|
||||
...(subPath ? { subPath } : {}),
|
||||
},
|
||||
{ name: "app", mountPath: "/cnb-app" },
|
||||
{ name: "tmp", mountPath: "/tmp" },
|
||||
],
|
||||
},
|
||||
],
|
||||
containers: [
|
||||
{
|
||||
// The existing Kubernetes observer selects this name for logs/status.
|
||||
name: "buildkit",
|
||||
image: builderImage,
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
command: ["/cnb/lifecycle/creator"],
|
||||
args: [
|
||||
"--app",
|
||||
"/cnb-app",
|
||||
"--layers",
|
||||
"/layers",
|
||||
"--platform",
|
||||
"/platform",
|
||||
...(packagePath
|
||||
? [
|
||||
"--buildpacks",
|
||||
"/custom-buildpacks",
|
||||
"--order",
|
||||
"/package/order.toml",
|
||||
]
|
||||
: []),
|
||||
"--cache-image",
|
||||
options.cacheImage,
|
||||
"--uid",
|
||||
"1000",
|
||||
"--gid",
|
||||
"1000",
|
||||
...[...insecureRegistries].flatMap((host) => [
|
||||
"--insecure-registry",
|
||||
host,
|
||||
]),
|
||||
...images.slice(1).flatMap((image) => ["--tag", image]),
|
||||
images[0],
|
||||
],
|
||||
env,
|
||||
securityContext: {
|
||||
allowPrivilegeEscalation: false,
|
||||
runAsNonRoot: true,
|
||||
runAsUser: 1000,
|
||||
runAsGroup: 1000,
|
||||
},
|
||||
volumeMounts: [...writableMounts, ...authMounts, ...customMounts],
|
||||
},
|
||||
],
|
||||
volumes: [
|
||||
{
|
||||
name: "workspace",
|
||||
persistentVolumeClaim: { claimName: options.workspaceClaimName },
|
||||
},
|
||||
...["app", "layers", "cache", "platform", "tmp"].map((name) => ({
|
||||
name,
|
||||
emptyDir: {},
|
||||
})),
|
||||
...(packagePath
|
||||
? [{ name: "custom-buildpacks", emptyDir: {} }]
|
||||
: []),
|
||||
...(options.registrySecretName
|
||||
? [
|
||||
{
|
||||
name: "registry-auth",
|
||||
secret: {
|
||||
secretName: options.registrySecretName,
|
||||
items: [
|
||||
{ key: ".dockerconfigjson", path: "config.json" },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user