diff --git a/package-lock.json b/package-lock.json index 1aa18d0e..719a2da2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3165,9 +3165,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" diff --git a/package.json b/package.json index 7b863b02..34f1f877 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "format": "prettier --write '**/*.ts'", "format-check": "prettier --check '**/*.ts'", "lint": "eslint packages/**/*.ts", - "lint:fix": "eslint packages/**/*.ts --fix", "build-all": "npm run build --prefix packages/hooklib && npm run build --prefix packages/k8s && npm run build --prefix packages/docker" }, "repository": { diff --git a/packages/k8s/package-lock.json b/packages/k8s/package-lock.json index b31c861e..9338a38c 100644 --- a/packages/k8s/package-lock.json +++ b/packages/k8s/package-lock.json @@ -4071,9 +4071,9 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "license": "ISC", "dependencies": { diff --git a/packages/k8s/package.json b/packages/k8s/package.json index 6821b7c1..3de3a0a1 100644 --- a/packages/k8s/package.json +++ b/packages/k8s/package.json @@ -15,6 +15,7 @@ "dependencies": { "@actions/core": "^1.11.1", "@actions/exec": "^1.1.1", + "@actions/github": "^6.0.0", "@actions/io": "^1.1.3", "@kubernetes/client-node": "^1.3.0", "hooklib": "file:../hooklib", diff --git a/packages/k8s/src/hooks/prepare-job.ts b/packages/k8s/src/hooks/prepare-job.ts index 28453c17..7ae8625a 100644 --- a/packages/k8s/src/hooks/prepare-job.ts +++ b/packages/k8s/src/hooks/prepare-job.ts @@ -49,6 +49,7 @@ export async function prepareJob( let container: k8s.V1Container | undefined = undefined if (args.container?.image) { + core.info(`Using image '${args.container.image}' for job image`) container = createContainerSpec( args.container, JOB_CONTAINER_NAME, diff --git a/packages/k8s/src/k8s/index.ts b/packages/k8s/src/k8s/index.ts index ae773da3..7bd3d6c1 100644 --- a/packages/k8s/src/k8s/index.ts +++ b/packages/k8s/src/k8s/index.ts @@ -1,6 +1,7 @@ import * as core from '@actions/core' import * as path from 'path' import { spawn } from 'child_process' +import { context as jobContext } from '@actions/github' import * as k8s from '@kubernetes/client-node' import tar from 'tar-fs' import * as stream from 'stream' @@ -62,6 +63,52 @@ export const requiredPermissions = [ } ] +/** + * Valid k8s labels conform to the following rules: + * - must be 63 characters or less (can be empty), + * - unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]), + * - could contain dashes (-), underscores (_), dots (.), and alphanumerics between + * @param label to be sanitized + * @returns sanitized label + */ +function sanitizeLabel(label: string): string { + const sluggedLabel = label + .replace(/[^a-zA-Z0-9\-_.]/g, '') // Strip disallowed characters + .replace(/^[^a-zA-Z0-9]+/, '') // Ensure it starts with an alphanumeric character + .replace(/[^a-zA-Z0-9]+$/, '') // Ensure it ends with an alphanumeric character + .substring(0, 63) // Truncate to 63 characters + + const kubernetesLabelRegex = /^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$/ + if (sluggedLabel !== '' && !kubernetesLabelRegex.test(sluggedLabel)) { + core.warning( + `Sanitized label "${sluggedLabel}" does not match Kubernetes label regex. Original: "${label}", ignoring label.` + ) + return '' + } + + return sluggedLabel +} + +function getArcContextLabels(): { [key: string]: string } { + const { GITHUB_RUN_ID, GITHUB_RUN_NUMBER, GITHUB_RUN_ATTEMPT } = process.env + return Object.fromEntries( + Object.entries({ + 'arc-context-event-name': jobContext.eventName, + 'arc-context-sha': jobContext.sha, + 'arc-context-workflow': jobContext.workflow, + 'arc-context-actor': jobContext.actor, + 'arc-context-job': jobContext.job, + 'arc-context-repository': jobContext.repo.repo, + 'arc-context-repository-owner': jobContext.repo.owner, + 'arc-context-run-id': GITHUB_RUN_ID || '', + 'arc-context-run-number': GITHUB_RUN_NUMBER || '', + 'arc-context-run-attempt': GITHUB_RUN_ATTEMPT || '' + }) + .map(([key, value]) => [key, sanitizeLabel(value)]) + .filter(([, value]) => value !== '') + ) +} + export async function createJobPod( name: string, jobContainer?: k8s.V1Container, @@ -85,10 +132,15 @@ export async function createJobPod( appPod.metadata = new k8s.V1ObjectMeta() appPod.metadata.name = name + const arcLabels = getArcContextLabels() const instanceLabel = new RunnerInstanceLabel() appPod.metadata.labels = { - [instanceLabel.key]: instanceLabel.value + [instanceLabel.key]: instanceLabel.value, + ...arcLabels } + + core.debug(`Pod labels: ${JSON.stringify(appPod.metadata.labels)}`) + appPod.metadata.annotations = {} appPod.spec = new k8s.V1PodSpec() @@ -196,9 +248,12 @@ export async function createContainerStepPod( appPod.metadata.name = name const instanceLabel = new RunnerInstanceLabel() + const arcLabels = getArcContextLabels() appPod.metadata.labels = { - [instanceLabel.key]: instanceLabel.value + [instanceLabel.key]: instanceLabel.value, + ...arcLabels } + appPod.metadata.annotations = {} appPod.spec = new k8s.V1PodSpec() @@ -527,7 +582,9 @@ export async function execCpFromPod( attempt++ if (attempt >= 30) { throw new Error( - `execCpFromPod failed after ${attempt} attempts: ${JSON.stringify(error)}` + `execCpFromPod failed after ${attempt} attempts: ${JSON.stringify( + error + )}` ) } await sleep(1000) @@ -697,7 +754,9 @@ export async function waitForPodPhases( } } catch (error) { throw new Error( - `Pod ${podName} is unhealthy with phase status ${phase}: ${JSON.stringify(error)}` + `Pod ${podName} is unhealthy with phase status ${phase}: ${JSON.stringify( + error + )}` ) } }