Adding more tests and minor changes in code (#21)

* added cleanup job checks, started testing constants file

* added getVolumeClaimName test

* added write entrypoint tests

* added tests around k8s utils

* fixed new regexp

* added tests around runner instance label

* 100% test coverage of constants
This commit is contained in:
Nikola Jokic
2022-06-22 11:15:42 -07:00
committed by GitHub
parent 20c19dae27
commit eb47baaf5e
5 changed files with 377 additions and 9 deletions

View File

@@ -39,8 +39,8 @@ export function getSecretName(): string {
)}-secret-${uuidv4().substring(0, STEP_POD_NAME_SUFFIX_LENGTH)}` )}-secret-${uuidv4().substring(0, STEP_POD_NAME_SUFFIX_LENGTH)}`
} }
const MAX_POD_NAME_LENGTH = 63 export const MAX_POD_NAME_LENGTH = 63
const STEP_POD_NAME_SUFFIX_LENGTH = 8 export const STEP_POD_NAME_SUFFIX_LENGTH = 8
export const JOB_CONTAINER_NAME = 'job' export const JOB_CONTAINER_NAME = 'job'
export class RunnerInstanceLabel { export class RunnerInstanceLabel {

View File

@@ -20,18 +20,18 @@ export function containerVolumes(
} }
] ]
const workspacePath = process.env.GITHUB_WORKSPACE as string
if (containerAction) { if (containerAction) {
const workspace = process.env.GITHUB_WORKSPACE as string
mounts.push( mounts.push(
{ {
name: POD_VOLUME_NAME, name: POD_VOLUME_NAME,
mountPath: '/github/workspace', mountPath: '/github/workspace',
subPath: workspace.substring(workspace.indexOf('work/') + 1) subPath: workspacePath.substring(workspacePath.indexOf('work/') + 1)
}, },
{ {
name: POD_VOLUME_NAME, name: POD_VOLUME_NAME,
mountPath: '/github/file_commands', mountPath: '/github/file_commands',
subPath: workspace.substring(workspace.indexOf('work/') + 1) subPath: workspacePath.substring(workspacePath.indexOf('work/') + 1)
} }
) )
return mounts return mounts
@@ -63,7 +63,6 @@ export function containerVolumes(
return mounts return mounts
} }
const workspacePath = process.env.GITHUB_WORKSPACE as string
for (const userVolume of userMountVolumes) { for (const userVolume of userMountVolumes) {
let sourceVolumePath = '' let sourceVolumePath = ''
if (path.isAbsolute(userVolume.sourceVolumePath)) { if (path.isAbsolute(userVolume.sourceVolumePath)) {

View File

@@ -1,4 +1,7 @@
import * as k8s from '@kubernetes/client-node'
import { cleanupJob, prepareJob } from '../src/hooks' import { cleanupJob, prepareJob } from '../src/hooks'
import { RunnerInstanceLabel } from '../src/hooks/constants'
import { namespace } from '../src/k8s'
import { TestHelper } from './test-setup' import { TestHelper } from './test-setup'
let testHelper: TestHelper let testHelper: TestHelper
@@ -13,10 +16,50 @@ describe('Cleanup Job', () => {
) )
await prepareJob(prepareJobData.args, prepareJobOutputFilePath) await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
}) })
it('should not throw', async () => {
await expect(cleanupJob()).resolves.not.toThrow()
})
afterEach(async () => { afterEach(async () => {
await testHelper.cleanup() await testHelper.cleanup()
}) })
it('should not throw', async () => {
await expect(cleanupJob()).resolves.not.toThrow()
})
it('should have no runner linked pods running', async () => {
await cleanupJob()
const kc = new k8s.KubeConfig()
kc.loadFromDefault()
const k8sApi = kc.makeApiClient(k8s.CoreV1Api)
const podList = await k8sApi.listNamespacedPod(
namespace(),
undefined,
undefined,
undefined,
undefined,
new RunnerInstanceLabel().toString()
)
expect(podList.body.items.length).toBe(0)
})
it('should have no runner linked secrets', async () => {
await cleanupJob()
const kc = new k8s.KubeConfig()
kc.loadFromDefault()
const k8sApi = kc.makeApiClient(k8s.CoreV1Api)
const secretList = await k8sApi.listNamespacedSecret(
namespace(),
undefined,
undefined,
undefined,
undefined,
new RunnerInstanceLabel().toString()
)
expect(secretList.body.items.length).toBe(0)
})
}) })

View File

@@ -0,0 +1,173 @@
import {
getJobPodName,
getRunnerPodName,
getSecretName,
getStepPodName,
getVolumeClaimName,
MAX_POD_NAME_LENGTH,
RunnerInstanceLabel,
STEP_POD_NAME_SUFFIX_LENGTH
} from '../src/hooks/constants'
describe('constants', () => {
describe('runner instance label', () => {
beforeEach(() => {
process.env.ACTIONS_RUNNER_POD_NAME = 'example'
})
it('should throw if ACTIONS_RUNNER_POD_NAME env is not set', () => {
delete process.env.ACTIONS_RUNNER_POD_NAME
expect(() => new RunnerInstanceLabel()).toThrow()
})
it('should have key truthy', () => {
const runnerInstanceLabel = new RunnerInstanceLabel()
expect(typeof runnerInstanceLabel.key).toBe('string')
expect(runnerInstanceLabel.key).toBeTruthy()
expect(runnerInstanceLabel.key.length).toBeGreaterThan(0)
})
it('should have value as runner pod name', () => {
const name = process.env.ACTIONS_RUNNER_POD_NAME as string
const runnerInstanceLabel = new RunnerInstanceLabel()
expect(typeof runnerInstanceLabel.value).toBe('string')
expect(runnerInstanceLabel.value).toBe(name)
})
it('should have toString combination of key and value', () => {
const runnerInstanceLabel = new RunnerInstanceLabel()
expect(runnerInstanceLabel.toString()).toBe(
`${runnerInstanceLabel.key}=${runnerInstanceLabel.value}`
)
})
})
describe('getRunnerPodName', () => {
it('should throw if ACTIONS_RUNNER_POD_NAME env is not set', () => {
delete process.env.ACTIONS_RUNNER_POD_NAME
expect(() => getRunnerPodName()).toThrow()
process.env.ACTIONS_RUNNER_POD_NAME = ''
expect(() => getRunnerPodName()).toThrow()
})
it('should return corrent ACTIONS_RUNNER_POD_NAME name', () => {
const name = 'example'
process.env.ACTIONS_RUNNER_POD_NAME = name
expect(getRunnerPodName()).toBe(name)
})
})
describe('getJobPodName', () => {
it('should throw on getJobPodName if ACTIONS_RUNNER_POD_NAME env is not set', () => {
delete process.env.ACTIONS_RUNNER_POD_NAME
expect(() => getJobPodName()).toThrow()
process.env.ACTIONS_RUNNER_POD_NAME = ''
expect(() => getRunnerPodName()).toThrow()
})
it('should contain suffix -workflow', () => {
const tableTests = [
{
podName: 'test',
expect: 'test-workflow'
},
{
// podName.length == 63
podName:
'abcdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
expect:
'abcdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-workflow'
}
]
for (const tt of tableTests) {
process.env.ACTIONS_RUNNER_POD_NAME = tt.podName
const actual = getJobPodName()
expect(actual).toBe(tt.expect)
}
})
})
describe('getVolumeClaimName', () => {
it('should throw if ACTIONS_RUNNER_POD_NAME env is not set', () => {
delete process.env.ACTIONS_RUNNER_CLAIM_NAME
delete process.env.ACTIONS_RUNNER_POD_NAME
expect(() => getVolumeClaimName()).toThrow()
process.env.ACTIONS_RUNNER_POD_NAME = ''
expect(() => getVolumeClaimName()).toThrow()
})
it('should return ACTIONS_RUNNER_CLAIM_NAME env if set', () => {
const claimName = 'testclaim'
process.env.ACTIONS_RUNNER_CLAIM_NAME = claimName
process.env.ACTIONS_RUNNER_POD_NAME = 'example'
expect(getVolumeClaimName()).toBe(claimName)
})
it('should contain suffix -work if ACTIONS_RUNNER_CLAIM_NAME is not set', () => {
delete process.env.ACTIONS_RUNNER_CLAIM_NAME
process.env.ACTIONS_RUNNER_POD_NAME = 'example'
expect(getVolumeClaimName()).toBe('example-work')
})
})
describe('getSecretName', () => {
it('should throw if ACTIONS_RUNNER_POD_NAME env is not set', () => {
delete process.env.ACTIONS_RUNNER_POD_NAME
expect(() => getSecretName()).toThrow()
process.env.ACTIONS_RUNNER_POD_NAME = ''
expect(() => getSecretName()).toThrow()
})
it('should contain suffix -secret- and name trimmed', () => {
const podNames = [
'test',
'abcdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
]
for (const podName of podNames) {
process.env.ACTIONS_RUNNER_POD_NAME = podName
const actual = getSecretName()
const re = new RegExp(
`${podName.substring(
MAX_POD_NAME_LENGTH -
'-secret-'.length -
STEP_POD_NAME_SUFFIX_LENGTH
)}-secret-[a-z0-9]{8,}`
)
expect(actual).toMatch(re)
}
})
})
describe('getStepPodName', () => {
it('should throw if ACTIONS_RUNNER_POD_NAME env is not set', () => {
delete process.env.ACTIONS_RUNNER_POD_NAME
expect(() => getStepPodName()).toThrow()
process.env.ACTIONS_RUNNER_POD_NAME = ''
expect(() => getStepPodName()).toThrow()
})
it('should contain suffix -step- and name trimmed', () => {
const podNames = [
'test',
'abcdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
]
for (const podName of podNames) {
process.env.ACTIONS_RUNNER_POD_NAME = podName
const actual = getStepPodName()
const re = new RegExp(
`${podName.substring(
MAX_POD_NAME_LENGTH - '-step-'.length - STEP_POD_NAME_SUFFIX_LENGTH
)}-step-[a-z0-9]{8,}`
)
expect(actual).toMatch(re)
}
})
})
})

View File

@@ -0,0 +1,153 @@
import * as fs from 'fs'
import { POD_VOLUME_NAME } from '../src/k8s'
import { containerVolumes, writeEntryPointScript } from '../src/k8s/utils'
import { TestHelper } from './test-setup'
let testHelper: TestHelper
describe('k8s utils', () => {
describe('write entrypoint', () => {
beforeEach(async () => {
testHelper = new TestHelper()
await testHelper.initialize()
})
afterEach(async () => {
await testHelper.cleanup()
})
it('should not throw', () => {
expect(() =>
writeEntryPointScript(
'/test',
'sh',
['-e', 'script.sh'],
['/prepend/path'],
{
SOME_ENV: 'SOME_VALUE'
}
)
).not.toThrow()
})
it('should throw if RUNNER_TEMP is not set', () => {
delete process.env.RUNNER_TEMP
expect(() =>
writeEntryPointScript(
'/test',
'sh',
['-e', 'script.sh'],
['/prepend/path'],
{
SOME_ENV: 'SOME_VALUE'
}
)
).toThrow()
})
it('should return object with containerPath and runnerPath', () => {
const { containerPath, runnerPath } = writeEntryPointScript(
'/test',
'sh',
['-e', 'script.sh'],
['/prepend/path'],
{
SOME_ENV: 'SOME_VALUE'
}
)
expect(containerPath).toMatch(/\/__w\/_temp\/.*\.sh/)
const re = new RegExp(`${process.env.RUNNER_TEMP}/.*\\.sh`)
expect(runnerPath).toMatch(re)
})
it('should write entrypoint path and the file should exist', () => {
const { runnerPath } = writeEntryPointScript(
'/test',
'sh',
['-e', 'script.sh'],
['/prepend/path'],
{
SOME_ENV: 'SOME_VALUE'
}
)
expect(fs.existsSync(runnerPath)).toBe(true)
})
})
describe('container volumes', () => {
beforeEach(async () => {
testHelper = new TestHelper()
await testHelper.initialize()
})
afterEach(async () => {
await testHelper.cleanup()
})
it('should throw if container action and GITHUB_WORKSPACE env is not set', () => {
delete process.env.GITHUB_WORKSPACE
expect(() => containerVolumes([], true, true)).toThrow()
expect(() => containerVolumes([], false, true)).toThrow()
})
it('should always have work mount', () => {
let volumes = containerVolumes([], true, true)
expect(volumes.find(e => e.mountPath === '/__w')).toBeTruthy()
volumes = containerVolumes([], true, false)
expect(volumes.find(e => e.mountPath === '/__w')).toBeTruthy()
volumes = containerVolumes([], false, true)
expect(volumes.find(e => e.mountPath === '/__w')).toBeTruthy()
volumes = containerVolumes([], false, false)
expect(volumes.find(e => e.mountPath === '/__w')).toBeTruthy()
})
it('should have container action volumes', () => {
let volumes = containerVolumes([], true, true)
expect(
volumes.find(e => e.mountPath === '/github/workspace')
).toBeTruthy()
expect(
volumes.find(e => e.mountPath === '/github/file_commands')
).toBeTruthy()
volumes = containerVolumes([], false, true)
expect(
volumes.find(e => e.mountPath === '/github/workspace')
).toBeTruthy()
expect(
volumes.find(e => e.mountPath === '/github/file_commands')
).toBeTruthy()
})
it('should have externals, github home and github workflow mounts if job container', () => {
const volumes = containerVolumes()
expect(volumes.find(e => e.mountPath === '/__e')).toBeTruthy()
expect(volumes.find(e => e.mountPath === '/github/home')).toBeTruthy()
expect(volumes.find(e => e.mountPath === '/github/workflow')).toBeTruthy()
})
it('should throw if user volume source volume path is not in workspace', () => {
expect(() =>
containerVolumes(
[
{
sourceVolumePath: '/outside/of/workdir'
}
],
true,
false
)
).toThrow()
})
it(`all volumes should have name ${POD_VOLUME_NAME}`, () => {
let volumes = containerVolumes([], true, true)
expect(volumes.every(e => e.name === POD_VOLUME_NAME)).toBeTruthy()
volumes = containerVolumes([], true, false)
expect(volumes.every(e => e.name === POD_VOLUME_NAME)).toBeTruthy()
volumes = containerVolumes([], false, true)
expect(volumes.every(e => e.name === POD_VOLUME_NAME)).toBeTruthy()
volumes = containerVolumes([], false, false)
expect(volumes.every(e => e.name === POD_VOLUME_NAME)).toBeTruthy()
})
})
})