mirror of
https://github.com/actions/runner-container-hooks.git
synced 2025-12-14 16:46:43 +00:00
* Add option to use kube scheduler This should only be used when rwx volumes are supported or when using a single node cluster. * Add option to set timeout for prepare job If the kube scheduler is used to hold jobs until sufficient resources are available, then prepare job needs to wait for a longer period until the workflow pod is running. This timeout will mostly need an increase in cases where many jobs are triggered which together exceed the resources available in the cluster. The workflows can then be gracefully handled later when sufficient resources become available again. * Skip name override warning when names match or job extension * Add guard for positive timeouts with a warning * Write out ReadWriteMany in full
159 lines
5.1 KiB
TypeScript
159 lines
5.1 KiB
TypeScript
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
import { cleanupJob } from '../src/hooks'
|
|
import { createContainerSpec, prepareJob } from '../src/hooks/prepare-job'
|
|
import { TestHelper } from './test-setup'
|
|
import {
|
|
ENV_HOOK_TEMPLATE_PATH,
|
|
ENV_USE_KUBE_SCHEDULER,
|
|
generateContainerName,
|
|
readExtensionFromFile
|
|
} from '../src/k8s/utils'
|
|
import { getPodByName } from '../src/k8s'
|
|
import { V1Container } from '@kubernetes/client-node'
|
|
import * as yaml from 'js-yaml'
|
|
import { JOB_CONTAINER_NAME } from '../src/hooks/constants'
|
|
|
|
jest.useRealTimers()
|
|
|
|
let testHelper: TestHelper
|
|
|
|
let prepareJobData: any
|
|
|
|
let prepareJobOutputFilePath: string
|
|
|
|
describe('Prepare job', () => {
|
|
beforeEach(async () => {
|
|
testHelper = new TestHelper()
|
|
await testHelper.initialize()
|
|
prepareJobData = testHelper.getPrepareJobDefinition()
|
|
prepareJobOutputFilePath = testHelper.createFile('prepare-job-output.json')
|
|
})
|
|
afterEach(async () => {
|
|
await cleanupJob()
|
|
await testHelper.cleanup()
|
|
})
|
|
|
|
it('should not throw exception', async () => {
|
|
await expect(
|
|
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
|
).resolves.not.toThrow()
|
|
})
|
|
|
|
it('should generate output file in JSON format', async () => {
|
|
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
|
const content = fs.readFileSync(prepareJobOutputFilePath)
|
|
expect(() => JSON.parse(content.toString())).not.toThrow()
|
|
})
|
|
|
|
it('should prepare job with absolute path for userVolumeMount', async () => {
|
|
prepareJobData.args.container.userMountVolumes = [
|
|
{
|
|
sourceVolumePath: path.join(
|
|
process.env.GITHUB_WORKSPACE as string,
|
|
'/myvolume'
|
|
),
|
|
targetVolumePath: '/volume_mount',
|
|
readOnly: false
|
|
}
|
|
]
|
|
await expect(
|
|
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
|
).resolves.not.toThrow()
|
|
})
|
|
|
|
it('should throw an exception if the user volume mount is absolute path outside of GITHUB_WORKSPACE', async () => {
|
|
prepareJobData.args.container.userMountVolumes = [
|
|
{
|
|
sourceVolumePath: '/somewhere/not/in/gh-workspace',
|
|
targetVolumePath: '/containermount',
|
|
readOnly: false
|
|
}
|
|
]
|
|
await expect(
|
|
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
|
).rejects.toThrow()
|
|
})
|
|
|
|
it('should not run prepare job without the job container', async () => {
|
|
prepareJobData.args.container = undefined
|
|
await expect(
|
|
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
|
).rejects.toThrow()
|
|
})
|
|
|
|
it('should not set command + args for service container if not passed in args', async () => {
|
|
const services = prepareJobData.args.services.map(service => {
|
|
return createContainerSpec(service, generateContainerName(service.image))
|
|
}) as [V1Container]
|
|
|
|
expect(services[0].command).toBe(undefined)
|
|
expect(services[0].args).toBe(undefined)
|
|
})
|
|
|
|
it('should run pod with extensions applied', async () => {
|
|
process.env[ENV_HOOK_TEMPLATE_PATH] = path.join(
|
|
__dirname,
|
|
'../../../examples/extension.yaml'
|
|
)
|
|
|
|
await expect(
|
|
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
|
).resolves.not.toThrow()
|
|
|
|
delete process.env[ENV_HOOK_TEMPLATE_PATH]
|
|
|
|
const content = JSON.parse(
|
|
fs.readFileSync(prepareJobOutputFilePath).toString()
|
|
)
|
|
|
|
const got = await getPodByName(content.state.jobPod)
|
|
|
|
expect(got.metadata?.annotations?.['annotated-by']).toBe('extension')
|
|
expect(got.metadata?.labels?.['labeled-by']).toBe('extension')
|
|
expect(got.spec?.securityContext?.runAsUser).toBe(1000)
|
|
expect(got.spec?.securityContext?.runAsGroup).toBe(3000)
|
|
|
|
// job container
|
|
expect(got.spec?.containers[0].name).toBe(JOB_CONTAINER_NAME)
|
|
expect(got.spec?.containers[0].image).toBe('node:14.16')
|
|
expect(got.spec?.containers[0].command).toEqual(['sh'])
|
|
expect(got.spec?.containers[0].args).toEqual(['-c', 'sleep 50'])
|
|
|
|
// service container
|
|
expect(got.spec?.containers[1].image).toBe('redis')
|
|
expect(got.spec?.containers[1].command).toBeFalsy()
|
|
expect(got.spec?.containers[1].args).toBeFalsy()
|
|
// side-car
|
|
expect(got.spec?.containers[2].name).toBe('side-car')
|
|
expect(got.spec?.containers[2].image).toBe('ubuntu:latest')
|
|
expect(got.spec?.containers[2].command).toEqual(['sh'])
|
|
expect(got.spec?.containers[2].args).toEqual(['-c', 'sleep 60'])
|
|
})
|
|
|
|
it('should not throw exception using kube scheduler', async () => {
|
|
// only for ReadWriteMany volumes or single node cluster
|
|
process.env[ENV_USE_KUBE_SCHEDULER] = 'true'
|
|
|
|
await expect(
|
|
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
|
).resolves.not.toThrow()
|
|
|
|
delete process.env[ENV_USE_KUBE_SCHEDULER]
|
|
})
|
|
|
|
test.each([undefined, null, []])(
|
|
'should not throw exception when portMapping=%p',
|
|
async pm => {
|
|
prepareJobData.args.services.forEach(s => {
|
|
s.portMappings = pm
|
|
})
|
|
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
|
const content = JSON.parse(
|
|
fs.readFileSync(prepareJobOutputFilePath).toString()
|
|
)
|
|
expect(() => content.context.services[0].image).not.toThrow()
|
|
}
|
|
)
|
|
})
|