Allow non-root container (#264)

* Allow non-root container

* format

* add lint:fix and fix lint errors

* fix tests and volume mounts
This commit is contained in:
Nikola Jokic
2025-11-21 14:44:29 +01:00
committed by GitHub
parent ad9cb43c31
commit 15e808935c
5 changed files with 63 additions and 4 deletions

View File

@@ -12,6 +12,7 @@
"format": "prettier --write '**/*.ts'", "format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'", "format-check": "prettier --check '**/*.ts'",
"lint": "eslint packages/**/*.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" "build-all": "npm run build --prefix packages/hooklib && npm run build --prefix packages/k8s && npm run build --prefix packages/docker"
}, },
"repository": { "repository": {

View File

@@ -20,8 +20,10 @@ import {
listDirAllCommand, listDirAllCommand,
sleep, sleep,
EXTERNALS_VOLUME_NAME, EXTERNALS_VOLUME_NAME,
GITHUB_VOLUME_NAME GITHUB_VOLUME_NAME,
WORK_VOLUME
} from './utils' } from './utils'
import * as shlex from 'shlex'
const kc = new k8s.KubeConfig() const kc = new k8s.KubeConfig()
@@ -91,13 +93,23 @@ export async function createJobPod(
appPod.spec = new k8s.V1PodSpec() appPod.spec = new k8s.V1PodSpec()
appPod.spec.containers = containers appPod.spec.containers = containers
appPod.spec.securityContext = {
fsGroup: 1001
}
appPod.spec.initContainers = [ appPod.spec.initContainers = [
{ {
name: 'fs-init', name: 'fs-init',
image: image:
process.env.ACTIONS_RUNNER_IMAGE || process.env.ACTIONS_RUNNER_IMAGE ||
'ghcr.io/actions/actions-runner:latest', '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: { securityContext: {
runAsGroup: 1001, runAsGroup: 1001,
runAsUser: 1001 runAsUser: 1001
@@ -106,6 +118,14 @@ export async function createJobPod(
{ {
name: EXTERNALS_VOLUME_NAME, name: EXTERNALS_VOLUME_NAME,
mountPath: '/mnt/externals' mountPath: '/mnt/externals'
},
{
name: WORK_VOLUME,
mountPath: '/mnt/work'
},
{
name: GITHUB_VOLUME_NAME,
mountPath: '/mnt/github'
} }
] ]
} }
@@ -121,6 +141,10 @@ export async function createJobPod(
{ {
name: GITHUB_VOLUME_NAME, name: GITHUB_VOLUME_NAME,
emptyDir: {} emptyDir: {}
},
{
name: WORK_VOLUME,
emptyDir: {}
} }
] ]
@@ -180,6 +204,10 @@ export async function createContainerStepPod(
{ {
name: GITHUB_VOLUME_NAME, name: GITHUB_VOLUME_NAME,
emptyDir: {} emptyDir: {}
},
{
name: WORK_VOLUME,
emptyDir: {}
} }
] ]
@@ -351,7 +379,15 @@ export async function execCpToPod(
while (true) { while (true) {
try { try {
const exec = new k8s.Exec(kc) 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 readStream = tar.pack(runnerPath)
const errStream = new WritableStreamBuffer() const errStream = new WritableStreamBuffer()
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@@ -369,7 +405,7 @@ export async function execCpToPod(
if (errStream.size()) { if (errStream.size()) {
reject( reject(
new Error( new Error(
`Error from cpFromPod - details: \n ${errStream.getContentsAsString()}` `Error from execCpToPod - status: ${status.status}, details: \n ${errStream.getContentsAsString()}`
) )
) )
} }

View File

@@ -15,12 +15,17 @@ export const ENV_USE_KUBE_SCHEDULER = 'ACTIONS_RUNNER_USE_KUBE_SCHEDULER'
export const EXTERNALS_VOLUME_NAME = 'externals' export const EXTERNALS_VOLUME_NAME = 'externals'
export const GITHUB_VOLUME_NAME = 'github' export const GITHUB_VOLUME_NAME = 'github'
export const WORK_VOLUME = 'work'
export const CONTAINER_VOLUMES: k8s.V1VolumeMount[] = [ export const CONTAINER_VOLUMES: k8s.V1VolumeMount[] = [
{ {
name: EXTERNALS_VOLUME_NAME, name: EXTERNALS_VOLUME_NAME,
mountPath: '/__e' mountPath: '/__e'
}, },
{
name: WORK_VOLUME,
mountPath: '/__w'
},
{ {
name: GITHUB_VOLUME_NAME, name: GITHUB_VOLUME_NAME,
mountPath: '/github' mountPath: '/github'

View File

@@ -26,6 +26,7 @@ describe('e2e', () => {
afterEach(async () => { afterEach(async () => {
await testHelper.cleanup() await testHelper.cleanup()
}) })
it('should prepare job, run script step, run container step then cleanup without errors', async () => { it('should prepare job, run script step, run container step then cleanup without errors', async () => {
await expect( await expect(
prepareJob(prepareJobData.args, prepareJobOutputFilePath) prepareJob(prepareJobData.args, prepareJobOutputFilePath)

View File

@@ -231,4 +231,20 @@ describe('Prepare job', () => {
expect(() => content.context.services[0].image).not.toThrow() 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()
)
expect(content.state.jobPod).toBeTruthy()
expect(content.context.container.image).toBe(
'ghcr.io/actions/actions-runner:latest'
)
})
}) })