From ece71182f4bee4710e91d0915f202bc40868a1fe Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Wed, 22 Oct 2025 14:16:22 +0200 Subject: [PATCH 1/4] Allow non-root container --- packages/k8s/src/k8s/index.ts | 36 +++++++++++++++++++++++--- packages/k8s/src/k8s/utils.ts | 5 ++++ packages/k8s/tests/prepare-job-test.ts | 12 +++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/k8s/src/k8s/index.ts b/packages/k8s/src/k8s/index.ts index c0fd250f..f30db762 100644 --- a/packages/k8s/src/k8s/index.ts +++ b/packages/k8s/src/k8s/index.ts @@ -20,8 +20,10 @@ import { listDirAllCommand, sleep, EXTERNALS_VOLUME_NAME, - GITHUB_VOLUME_NAME + GITHUB_VOLUME_NAME, + WORK_VOLUME } from './utils' +import * as shlex from 'shlex' const kc = new k8s.KubeConfig() @@ -91,13 +93,21 @@ export async function createJobPod( appPod.spec = new k8s.V1PodSpec() appPod.spec.containers = containers + appPod.spec.securityContext = { + fsGroup: 1001 + } appPod.spec.initContainers = [ { name: 'fs-init', image: process.env.ACTIONS_RUNNER_IMAGE || 'ghcr.io/actions/actions-runner:latest', - command: ['sh', '-c', 'mv /home/runner/externals/* /mnt/externals'], + command: ['sh', '-c', + `mkdir -p /mnt/externals && \\ + mkdir -p /mnt/work && \\ + mkdir -p /mnt/github && \\ + mv /home/runner/externals/* /mnt/externals/` + ], securityContext: { runAsGroup: 1001, runAsUser: 1001 @@ -106,6 +116,14 @@ export async function createJobPod( { name: EXTERNALS_VOLUME_NAME, mountPath: '/mnt/externals' + }, + { + name: WORK_VOLUME, + mountPath: '/mnt/work' + }, + { + name: GITHUB_VOLUME_NAME, + mountPath: '/mnt/github' } ] } @@ -121,6 +139,10 @@ export async function createJobPod( { name: GITHUB_VOLUME_NAME, emptyDir: {} + }, + { + name: WORK_VOLUME, + emptyDir: {} } ] @@ -351,7 +373,13 @@ export async function execCpToPod( while (true) { try { const exec = new k8s.Exec(kc) - const command = ['tar', 'xf', '-', '-C', containerPath] + // Use tar to extract with --no-same-owner to avoid ownership issues. + // Then use find to fix permissions. The -m flag helps but we also need to fix permissions after. + const command = ['sh', '-c', + `tar xf - --no-same-owner -C ${shlex.quote(containerPath)} 2>/dev/null; ` + + `find ${shlex.quote(containerPath)} -type f -exec chmod u+rw {} \\; 2>/dev/null; ` + + `find ${shlex.quote(containerPath)} -type d -exec chmod u+rwx {} \\; 2>/dev/null` + ] const readStream = tar.pack(runnerPath) const errStream = new WritableStreamBuffer() await new Promise((resolve, reject) => { @@ -369,7 +397,7 @@ export async function execCpToPod( if (errStream.size()) { reject( new Error( - `Error from cpFromPod - details: \n ${errStream.getContentsAsString()}` + `Error from execCpToPod - status: ${status.status}, details: \n ${errStream.getContentsAsString()}` ) ) } diff --git a/packages/k8s/src/k8s/utils.ts b/packages/k8s/src/k8s/utils.ts index a391d64e..25428ffa 100644 --- a/packages/k8s/src/k8s/utils.ts +++ b/packages/k8s/src/k8s/utils.ts @@ -15,12 +15,17 @@ export const ENV_USE_KUBE_SCHEDULER = 'ACTIONS_RUNNER_USE_KUBE_SCHEDULER' export const EXTERNALS_VOLUME_NAME = 'externals' export const GITHUB_VOLUME_NAME = 'github' +export const WORK_VOLUME = 'work' export const CONTAINER_VOLUMES: k8s.V1VolumeMount[] = [ { name: EXTERNALS_VOLUME_NAME, mountPath: '/__e' }, + { + name: WORK_VOLUME, + mountPath: '/__w' + }, { name: GITHUB_VOLUME_NAME, mountPath: '/github' diff --git a/packages/k8s/tests/prepare-job-test.ts b/packages/k8s/tests/prepare-job-test.ts index 714e211f..5ff282da 100644 --- a/packages/k8s/tests/prepare-job-test.ts +++ b/packages/k8s/tests/prepare-job-test.ts @@ -231,4 +231,16 @@ describe('Prepare job', () => { expect(() => content.context.services[0].image).not.toThrow() } ) + + it('should prepare job with container with non-root user', async () => { + prepareJobData.args!.container!.image = + 'ghcr.io/actions/actions-runner:latest' // known to use user 1001 + await expect( + prepareJob(prepareJobData.args!, prepareJobOutputFilePath) + ).resolves.not.toThrow() + + const content = JSON.parse( + fs.readFileSync(prepareJobOutputFilePath).toString() + ) + }) }) From ff037081161d22450e80ffe8aa010a098ac2ade9 Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Thu, 23 Oct 2025 12:30:46 +0200 Subject: [PATCH 2/4] format --- packages/k8s/src/k8s/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/k8s/src/k8s/index.ts b/packages/k8s/src/k8s/index.ts index f30db762..80ecdfbd 100644 --- a/packages/k8s/src/k8s/index.ts +++ b/packages/k8s/src/k8s/index.ts @@ -102,7 +102,9 @@ export async function createJobPod( image: process.env.ACTIONS_RUNNER_IMAGE || 'ghcr.io/actions/actions-runner:latest', - command: ['sh', '-c', + command: [ + 'sh', + '-c', `mkdir -p /mnt/externals && \\ mkdir -p /mnt/work && \\ mkdir -p /mnt/github && \\ @@ -375,10 +377,12 @@ export async function execCpToPod( const exec = new k8s.Exec(kc) // Use tar to extract with --no-same-owner to avoid ownership issues. // Then use find to fix permissions. The -m flag helps but we also need to fix permissions after. - const command = ['sh', '-c', + const command = [ + 'sh', + '-c', `tar xf - --no-same-owner -C ${shlex.quote(containerPath)} 2>/dev/null; ` + - `find ${shlex.quote(containerPath)} -type f -exec chmod u+rw {} \\; 2>/dev/null; ` + - `find ${shlex.quote(containerPath)} -type d -exec chmod u+rwx {} \\; 2>/dev/null` + `find ${shlex.quote(containerPath)} -type f -exec chmod u+rw {} \\; 2>/dev/null; ` + + `find ${shlex.quote(containerPath)} -type d -exec chmod u+rwx {} \\; 2>/dev/null` ] const readStream = tar.pack(runnerPath) const errStream = new WritableStreamBuffer() From ef8e11fe0941f9c63ee77373c9de387d8be299fc Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Tue, 4 Nov 2025 11:57:39 +0100 Subject: [PATCH 3/4] add lint:fix and fix lint errors --- package.json | 1 + packages/k8s/tests/prepare-job-test.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 34f1f877..7b863b02 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "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/tests/prepare-job-test.ts b/packages/k8s/tests/prepare-job-test.ts index 5ff282da..29f6d5e5 100644 --- a/packages/k8s/tests/prepare-job-test.ts +++ b/packages/k8s/tests/prepare-job-test.ts @@ -236,11 +236,11 @@ describe('Prepare job', () => { prepareJobData.args!.container!.image = 'ghcr.io/actions/actions-runner:latest' // known to use user 1001 await expect( - prepareJob(prepareJobData.args!, prepareJobOutputFilePath) + prepareJob(prepareJobData.args, prepareJobOutputFilePath) ).resolves.not.toThrow() - const content = JSON.parse( + JSON.parse( fs.readFileSync(prepareJobOutputFilePath).toString() - ) + ).resolves.not.toThrow() }) }) From 7565566310169aa011797ca478581516d59e6abb Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Fri, 21 Nov 2025 13:00:31 +0100 Subject: [PATCH 4/4] fix tests and volume mounts --- packages/k8s/src/k8s/index.ts | 4 ++++ packages/k8s/tests/e2e-test.ts | 1 + packages/k8s/tests/prepare-job-test.ts | 8 ++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/k8s/src/k8s/index.ts b/packages/k8s/src/k8s/index.ts index 80ecdfbd..311a44ab 100644 --- a/packages/k8s/src/k8s/index.ts +++ b/packages/k8s/src/k8s/index.ts @@ -204,6 +204,10 @@ export async function createContainerStepPod( { name: GITHUB_VOLUME_NAME, emptyDir: {} + }, + { + name: WORK_VOLUME, + emptyDir: {} } ] diff --git a/packages/k8s/tests/e2e-test.ts b/packages/k8s/tests/e2e-test.ts index 193b7231..80976955 100644 --- a/packages/k8s/tests/e2e-test.ts +++ b/packages/k8s/tests/e2e-test.ts @@ -26,6 +26,7 @@ describe('e2e', () => { afterEach(async () => { await testHelper.cleanup() }) + it('should prepare job, run script step, run container step then cleanup without errors', async () => { await expect( prepareJob(prepareJobData.args, prepareJobOutputFilePath) diff --git a/packages/k8s/tests/prepare-job-test.ts b/packages/k8s/tests/prepare-job-test.ts index 29f6d5e5..741b591d 100644 --- a/packages/k8s/tests/prepare-job-test.ts +++ b/packages/k8s/tests/prepare-job-test.ts @@ -239,8 +239,12 @@ describe('Prepare job', () => { prepareJob(prepareJobData.args, prepareJobOutputFilePath) ).resolves.not.toThrow() - JSON.parse( + const content = JSON.parse( fs.readFileSync(prepareJobOutputFilePath).toString() - ).resolves.not.toThrow() + ) + expect(content.state.jobPod).toBeTruthy() + expect(content.context.container.image).toBe( + 'ghcr.io/actions/actions-runner:latest' + ) }) })