Fix working directory and write state for appPod to be used in run-script-step (#8)

* added initial entrypoint script

* change workingg directory working with addition to fix prepare-job state output

* added prepend path

* added run-script-step file generation, removed prepend path from container-step and prepare job

* latest changes with testing run script step

* fix the mounts real fast

* cleanup

* fix tests

* add kind test

* add kind yaml to ignore and run it during ci

* fix kind option

* remove gitignore

* lowercase pwd

* checkout first!

* ignore test file in build.yaml

* fixed wrong working directory and added test to run script step testing for the env

* handle env's/escaping better

* added single quote escape to env escapes

* surounded env value with single quote

* added spacing around run-container-step, changed examples to actually echo hello world

* refactored tests

* make sure to escape properly

* set addition mounts for container steps

* fixup container action mounts

Co-authored-by: Thomas Boop <thboop@github.com>
Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
This commit is contained in:
Nikola Jokic
2022-06-15 03:41:49 +02:00
committed by GitHub
parent 643bf36fd8
commit 8ea57170d8
23 changed files with 391 additions and 230 deletions

View File

@@ -1,24 +1,16 @@
import * as path from 'path'
import * as fs from 'fs'
import { prepareJob, cleanupJob } from '../src/hooks'
import { cleanupJob, prepareJob } from '../src/hooks'
import { TestHelper } from './test-setup'
let testHelper: TestHelper
const prepareJobJsonPath = path.resolve(
`${__dirname}/../../../examples/prepare-job.json`
)
let prepareJobOutputFilePath: string
describe('Cleanup Job', () => {
beforeEach(async () => {
const prepareJobJson = fs.readFileSync(prepareJobJsonPath)
let prepareJobData = JSON.parse(prepareJobJson.toString())
testHelper = new TestHelper()
await testHelper.initialize()
prepareJobOutputFilePath = testHelper.createFile('prepare-job-output.json')
let prepareJobData = testHelper.getPrepareJobDefinition()
const prepareJobOutputFilePath = testHelper.createFile(
'prepare-job-output.json'
)
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
})
it('should not throw', async () => {

View File

@@ -1,5 +1,4 @@
import * as fs from 'fs'
import * as path from 'path'
import {
cleanupJob,
prepareJob,
@@ -12,26 +11,15 @@ jest.useRealTimers()
let testHelper: TestHelper
const prepareJobJsonPath = path.resolve(
`${__dirname}/../../../examples/prepare-job.json`
)
const runScriptStepJsonPath = path.resolve(
`${__dirname}/../../../examples/run-script-step.json`
)
let runContainerStepJsonPath = path.resolve(
`${__dirname}/../../../examples/run-container-step.json`
)
let prepareJobData: any
let prepareJobOutputFilePath: string
describe('e2e', () => {
beforeEach(async () => {
const prepareJobJson = fs.readFileSync(prepareJobJsonPath)
prepareJobData = JSON.parse(prepareJobJson.toString())
testHelper = new TestHelper()
await testHelper.initialize()
prepareJobData = testHelper.getPrepareJobDefinition()
prepareJobOutputFilePath = testHelper.createFile('prepare-job-output.json')
})
afterEach(async () => {
@@ -42,8 +30,7 @@ describe('e2e', () => {
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
).resolves.not.toThrow()
const scriptStepContent = fs.readFileSync(runScriptStepJsonPath)
const scriptStepData = JSON.parse(scriptStepContent.toString())
const scriptStepData = testHelper.getRunScriptStepDefinition()
const prepareJobOutputJson = fs.readFileSync(prepareJobOutputFilePath)
const prepareJobOutputData = JSON.parse(prepareJobOutputJson.toString())
@@ -52,8 +39,7 @@ describe('e2e', () => {
runScriptStep(scriptStepData.args, prepareJobOutputData.state, null)
).resolves.not.toThrow()
const runContainerStepContent = fs.readFileSync(runContainerStepJsonPath)
const runContainerStepData = JSON.parse(runContainerStepContent.toString())
const runContainerStepData = testHelper.getRunContainerStepDefinition()
await expect(
runContainerStep(runContainerStepData.args)

View File

@@ -8,20 +8,15 @@ jest.useRealTimers()
let testHelper: TestHelper
const prepareJobJsonPath = path.resolve(
`${__dirname}/../../../examples/prepare-job.json`
)
let prepareJobData: any
let prepareJobOutputFilePath: string
describe('Prepare job', () => {
beforeEach(async () => {
const prepareJobJson = fs.readFileSync(prepareJobJsonPath)
prepareJobData = JSON.parse(prepareJobJson.toString())
testHelper = new TestHelper()
await testHelper.initialize()
prepareJobData = testHelper.getPrepareJobDefinition()
prepareJobOutputFilePath = testHelper.createFile('prepare-job-output.json')
})
afterEach(async () => {
@@ -42,28 +37,29 @@ describe('Prepare job', () => {
})
it('should prepare job with absolute path for userVolumeMount', async () => {
prepareJobData.args.container.userMountVolumes.forEach(v => {
if (!path.isAbsolute(v.sourceVolumePath)) {
v.sourceVolumePath = path.join(
prepareJobData.args.container.userMountVolumes = [
{
sourceVolumePath: path.join(
process.env.GITHUB_WORKSPACE as string,
v.sourceVolumePath
)
'/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.forEach(v => {
if (!path.isAbsolute(v.sourceVolumePath)) {
v.sourceVolumePath = path.join(
'/path/outside/of/github-workspace',
v.sourceVolumePath
)
prepareJobData.args.container.userMountVolumes = [
{
sourceVolumePath: '/somewhere/not/in/gh-workspace',
targetVolumePath: '/containermount',
readOnly: false
}
})
]
await expect(
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
).rejects.toThrow()

View File

@@ -1,5 +1,3 @@
import * as fs from 'fs'
import * as path from 'path'
import { runContainerStep } from '../src/hooks'
import { TestHelper } from './test-setup'
@@ -7,25 +5,37 @@ jest.useRealTimers()
let testHelper: TestHelper
let runContainerStepJsonPath = path.resolve(
`${__dirname}/../../../examples/run-container-step.json`
)
let runContainerStepData: any
describe('Run container step', () => {
beforeAll(async () => {
const content = fs.readFileSync(runContainerStepJsonPath)
runContainerStepData = JSON.parse(content.toString())
beforeEach(async () => {
testHelper = new TestHelper()
await testHelper.initialize()
runContainerStepData = testHelper.getRunContainerStepDefinition()
})
afterEach(async () => {
await testHelper.cleanup()
})
it('should not throw', async () => {
const exitCode = await runContainerStep(runContainerStepData.args)
expect(exitCode).toBe(0)
})
it('should fail if the working directory does not exist', async () => {
runContainerStepData.args.workingDirectory = '/foo/bar'
await expect(runContainerStep(runContainerStepData.args)).rejects.toThrow()
})
it('should shold have env variables available', async () => {
runContainerStepData.args.entryPoint = 'bash'
runContainerStepData.args.entryPointArgs = [
'-c',
"'if [[ -z $NODE_ENV ]]; then exit 1; fi'"
]
await expect(
runContainerStep(runContainerStepData.args)
).resolves.not.toThrow()
})
afterEach(async () => {
await testHelper.cleanup()
})
})

View File

@@ -1,5 +1,4 @@
import * as fs from 'fs'
import * as path from 'path'
import { cleanupJob, prepareJob, runScriptStep } from '../src/hooks'
import { TestHelper } from './test-setup'
@@ -7,22 +6,21 @@ jest.useRealTimers()
let testHelper: TestHelper
const prepareJobJsonPath = path.resolve(
`${__dirname}/../../../examples/prepare-job.json`
)
let prepareJobData: any
let prepareJobOutputFilePath: string
let prepareJobOutputData: any
let runScriptStepDefinition
describe('Run script step', () => {
beforeEach(async () => {
const prepareJobJson = fs.readFileSync(prepareJobJsonPath)
prepareJobData = JSON.parse(prepareJobJson.toString())
testHelper = new TestHelper()
await testHelper.initialize()
prepareJobOutputFilePath = testHelper.createFile('prepare-job-output.json')
const prepareJobOutputFilePath = testHelper.createFile(
'prepare-job-output.json'
)
const prepareJobData = testHelper.getPrepareJobDefinition()
runScriptStepDefinition = testHelper.getRunScriptStepDefinition()
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
const outputContent = fs.readFileSync(prepareJobOutputFilePath)
prepareJobOutputData = JSON.parse(outputContent.toString())
@@ -38,21 +36,39 @@ describe('Run script step', () => {
// npm run test run-script-step
it('should not throw an exception', async () => {
const args = {
entryPointArgs: ['-c', 'echo "test"'],
entryPoint: 'bash',
environmentVariables: {
NODE_ENV: 'development'
},
prependPath: ['/foo/bar', 'bar/foo'],
workingDirectory: '/__w/repo/repo'
}
const state = {
jobPod: prepareJobOutputData.state.jobPod
}
const responseFile = null
await expect(
runScriptStep(args, state, responseFile)
runScriptStep(
runScriptStepDefinition.args,
prepareJobOutputData.state,
null
)
).resolves.not.toThrow()
})
it('should fail if the working directory does not exist', async () => {
runScriptStepDefinition.args.workingDirectory = '/foo/bar'
await expect(
runScriptStep(
runScriptStepDefinition.args,
prepareJobOutputData.state,
null
)
).rejects.toThrow()
})
it('should shold have env variables available', async () => {
runScriptStepDefinition.args.entryPoint = 'bash'
runScriptStepDefinition.args.entryPointArgs = [
'-c',
"'if [[ -z $NODE_ENV ]]; then exit 1; fi'"
]
await expect(
runScriptStep(
runScriptStepDefinition.args,
prepareJobOutputData.state,
null
)
).resolves.not.toThrow()
})
})

View File

@@ -0,0 +1,18 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
# add a mount from /path/to/my/files on the host to /files on the node
extraMounts:
- hostPath: {{PATHTOREPO}}
containerPath: {{PATHTOREPO}}
# optional: if set, the mount is read-only.
# default false
readOnly: false
# optional: if set, the mount needs SELinux relabeling.
# default false
selinuxRelabel: false
# optional: set propagation mode (None, HostToContainer or Bidirectional)
# see https://kubernetes.io/docs/concepts/storage/volumes/#mount-propagation
# default None
propagation: None

View File

@@ -1,5 +1,7 @@
import * as k8s from '@kubernetes/client-node'
import * as fs from 'fs'
import { HookData } from 'hooklib/lib'
import * as path from 'path'
import { v4 as uuidv4 } from 'uuid'
const kc = new k8s.KubeConfig()
@@ -20,17 +22,27 @@ export class TestHelper {
public async initialize(): Promise<void> {
process.env['ACTIONS_RUNNER_POD_NAME'] = `${this.podName}`
process.env['ACTIONS_RUNNER_CLAIM_NAME'] = `${this.podName}-work`
process.env['RUNNER_WORKSPACE'] = `${this.tempDirPath}/work/repo`
process.env['GITHUB_WORKSPACE'] = `${this.tempDirPath}/work/repo/repo`
process.env['RUNNER_WORKSPACE'] = `${this.tempDirPath}/_work/repo`
process.env['RUNNER_TEMP'] = `${this.tempDirPath}/_work/_temp`
process.env['GITHUB_WORKSPACE'] = `${this.tempDirPath}/_work/repo/repo`
process.env['ACTIONS_RUNNER_KUBERNETES_NAMESPACE'] = 'default'
fs.mkdirSync(`${this.tempDirPath}/_work/repo/repo`, { recursive: true })
fs.mkdirSync(`${this.tempDirPath}/externals`, { recursive: true })
fs.mkdirSync(process.env.RUNNER_TEMP, { recursive: true })
fs.copyFileSync(
path.resolve(`${__dirname}/../../../examples/example-script.sh`),
`${process.env.RUNNER_TEMP}/example-script.sh`
)
await this.cleanupK8sResources()
try {
await this.createTestVolume()
await this.createTestJobPod()
} catch {}
fs.mkdirSync(`${this.tempDirPath}/work/repo/repo`, { recursive: true })
fs.mkdirSync(`${this.tempDirPath}/externals`, { recursive: true })
} catch (e) {
console.log(JSON.stringify(e))
}
}
public async cleanup(): Promise<void> {
@@ -116,7 +128,7 @@ export class TestHelper {
volumeMode: 'Filesystem',
accessModes: ['ReadWriteOnce'],
hostPath: {
path: this.tempDirPath
path: `${this.tempDirPath}/_work`
}
}
}
@@ -139,4 +151,47 @@ export class TestHelper {
}
await k8sApi.createNamespacedPersistentVolumeClaim('default', volumeClaim)
}
public getPrepareJobDefinition(): HookData {
const prepareJob = JSON.parse(
fs.readFileSync(
path.resolve(__dirname + '/../../../examples/prepare-job.json'),
'utf8'
)
)
prepareJob.args.container.userMountVolumes = undefined
prepareJob.args.container.registry = null
prepareJob.args.services.forEach(s => {
s.registry = null
})
return prepareJob
}
public getRunScriptStepDefinition(): HookData {
const runScriptStep = JSON.parse(
fs.readFileSync(
path.resolve(__dirname + '/../../../examples/run-script-step.json'),
'utf8'
)
)
runScriptStep.args.entryPointArgs[1] = `/__w/_temp/example-script.sh`
return runScriptStep
}
public getRunContainerStepDefinition(): HookData {
const runContainerStep = JSON.parse(
fs.readFileSync(
path.resolve(__dirname + '/../../../examples/run-container-step.json'),
'utf8'
)
)
runContainerStep.args.entryPointArgs[1] = `/__w/_temp/example-script.sh`
runContainerStep.args.userMountVolumes = undefined
runContainerStep.args.registry = null
return runContainerStep
}
}