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

@@ -10,8 +10,12 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: helm/kind-action@v1.2.0
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- run: sed -i "s|{{PATHTOREPO}}|$(pwd)|" packages/k8s/tests/test-kind.yaml
name: Setup kind cluster yaml config
- uses: helm/kind-action@v1.2.0
with:
config: packages/k8s/tests/test-kind.yaml
- run: npm install - run: npm install
name: Install dependencies name: Install dependencies
- run: npm run bootstrap - run: npm run bootstrap
@@ -22,6 +26,6 @@ jobs:
- name: Check linter - name: Check linter
run: | run: |
npm run lint npm run lint
git diff --exit-code git diff --exit-code -- ':!packages/k8s/tests/test-kind.yaml'
- name: Run tests - name: Run tests
run: npm run test run: npm run test

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
node_modules/ node_modules/
lib/ lib/
dist/ dist/
**/tests/_temp/** **/tests/_temp/**
packages/k8s/tests/test-kind.yaml

View File

@@ -0,0 +1,3 @@
#!/bin/bash
echo "Hello World"

View File

@@ -12,8 +12,8 @@
"image": "node:14.16", "image": "node:14.16",
"dockerfile": null, "dockerfile": null,
"entryPointArgs": [ "entryPointArgs": [
"-c", "-e",
"echo \"hello world2\"" "example-script.sh"
], ],
"entryPoint": "bash", "entryPoint": "bash",
"workingDirectory": "/__w/repo/repo", "workingDirectory": "/__w/repo/repo",

View File

@@ -10,8 +10,8 @@
}, },
"args": { "args": {
"entryPointArgs": [ "entryPointArgs": [
"-c", "-e",
"echo \"hello world\"" "example-script.sh"
], ],
"entryPoint": "bash", "entryPoint": "bash",
"environmentVariables": { "environmentVariables": {

View File

@@ -1,4 +1,4 @@
import * as fs from 'fs' import { PrepareJobArgs } from 'hooklib/lib'
import { cleanupJob, prepareJob } from '../src/hooks' import { cleanupJob, prepareJob } from '../src/hooks'
import TestSetup from './test-setup' import TestSetup from './test-setup'
@@ -11,22 +11,16 @@ describe('cleanup job', () => {
testSetup = new TestSetup() testSetup = new TestSetup()
testSetup.initialize() testSetup.initialize()
const prepareJobDefinition = JSON.parse( const prepareJobDefinition = testSetup.getPrepareJobDefinition()
fs.readFileSync(
`${__dirname}/../../../examples/prepare-job.json`,
'utf-8'
)
)
const prepareJobOutput = testSetup.createOutputFile( const prepareJobOutput = testSetup.createOutputFile(
'prepare-job-output.json' 'prepare-job-output.json'
) )
prepareJobDefinition.args.container.registry = null await prepareJob(
prepareJobDefinition.args.services.forEach(s => { prepareJobDefinition.args as PrepareJobArgs,
s.registry = null prepareJobOutput
}) )
await prepareJob(prepareJobDefinition.args, prepareJobOutput)
}) })
afterEach(() => { afterEach(() => {

View File

@@ -1,24 +1,15 @@
import * as fs from 'fs'
import { containerBuild } from '../src/dockerCommands' import { containerBuild } from '../src/dockerCommands'
import TestSetup from './test-setup' import TestSetup from './test-setup'
let testSetup let testSetup
let runContainerStepDefinition let runContainerStepDefinition
const runContainerStepInputPath = `${__dirname}/../../../examples/run-container-step.json`
describe('container build', () => { describe('container build', () => {
beforeEach(() => { beforeEach(() => {
testSetup = new TestSetup() testSetup = new TestSetup()
testSetup.initialize() testSetup.initialize()
let runContainerStepJson = fs.readFileSync( runContainerStepDefinition = testSetup.getRunContainerStepDefinition()
runContainerStepInputPath,
'utf8'
)
runContainerStepDefinition = JSON.parse(runContainerStepJson.toString())
runContainerStepDefinition.image = ''
const actionPath = testSetup.initializeDockerAction()
runContainerStepDefinition.dockerfile = `${actionPath}/Dockerfile`
}) })
afterEach(() => { afterEach(() => {
@@ -26,6 +17,9 @@ describe('container build', () => {
}) })
it('should build container', async () => { it('should build container', async () => {
runContainerStepDefinition.image = ''
const actionPath = testSetup.initializeDockerAction()
runContainerStepDefinition.dockerfile = `${actionPath}/Dockerfile`
await expect( await expect(
containerBuild(runContainerStepDefinition, 'example-test-tag') containerBuild(runContainerStepDefinition, 'example-test-tag')
).resolves.not.toThrow() ).resolves.not.toThrow()

View File

@@ -1,5 +1,4 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path'
import { import {
cleanupJob, cleanupJob,
prepareJob, prepareJob,
@@ -8,28 +7,7 @@ import {
} from '../src/hooks' } from '../src/hooks'
import TestSetup from './test-setup' import TestSetup from './test-setup'
const definitions = { let definitions
prepareJob: JSON.parse(
fs.readFileSync(
path.resolve(__dirname + '/../../../examples/prepare-job.json'),
'utf8'
)
),
runContainerStep: JSON.parse(
fs.readFileSync(
path.resolve(__dirname + '/../../../examples/run-container-step.json'),
'utf8'
)
),
runScriptStep: JSON.parse(
fs.readFileSync(
path.resolve(__dirname + '/../../../examples/run-script-step.json'),
'utf-8'
)
)
}
let testSetup: TestSetup let testSetup: TestSetup
@@ -37,12 +15,12 @@ describe('e2e', () => {
beforeEach(() => { beforeEach(() => {
testSetup = new TestSetup() testSetup = new TestSetup()
testSetup.initialize() testSetup.initialize()
definitions.prepareJob.args.container.systemMountVolumes =
testSetup.systemMountVolumes definitions = {
definitions.prepareJob.args.container.registry = null prepareJob: testSetup.getPrepareJobDefinition(),
definitions.prepareJob.args.services.forEach(s => { runScriptStep: testSetup.getRunScriptStepDefinition(),
s.registry = null runContainerStep: testSetup.getRunContainerStepDefinition()
}) }
}) })
afterEach(() => { afterEach(() => {

View File

@@ -4,9 +4,7 @@ import TestSetup from './test-setup'
jest.useRealTimers() jest.useRealTimers()
const prepareJobDefinition = JSON.parse( let prepareJobDefinition
fs.readFileSync(`${__dirname}/../../../examples/prepare-job.json`, 'utf-8')
)
let testSetup: TestSetup let testSetup: TestSetup
@@ -14,15 +12,7 @@ describe('prepare job', () => {
beforeEach(() => { beforeEach(() => {
testSetup = new TestSetup() testSetup = new TestSetup()
testSetup.initialize() testSetup.initialize()
prepareJobDefinition = testSetup.getPrepareJobDefinition()
prepareJobDefinition.args.container.systemMountVolumes =
testSetup.systemMountVolumes
prepareJobDefinition.args.container.workingDirectory =
testSetup.workingDirectory
prepareJobDefinition.args.container.registry = null
prepareJobDefinition.args.services.forEach(s => {
s.registry = null
})
}) })
afterEach(() => { afterEach(() => {

View File

@@ -1,6 +1,5 @@
import * as fs from 'fs' import * as fs from 'fs'
import { PrepareJobResponse } from 'hooklib/lib' import { PrepareJobResponse } from 'hooklib/lib'
import * as path from 'path'
import { prepareJob, runScriptStep } from '../src/hooks' import { prepareJob, runScriptStep } from '../src/hooks'
import TestSetup from './test-setup' import TestSetup from './test-setup'
@@ -8,36 +7,23 @@ jest.useRealTimers()
let testSetup: TestSetup let testSetup: TestSetup
const definitions = { let definitions
prepareJob: JSON.parse(
fs.readFileSync(
path.resolve(__dirname + '/../../../examples/prepare-job.json'),
'utf8'
)
),
runScriptStep: JSON.parse(
fs.readFileSync(
path.resolve(__dirname + '/../../../examples/run-script-step.json'),
'utf-8'
)
)
}
let prepareJobResponse: PrepareJobResponse let prepareJobResponse: PrepareJobResponse
describe('run-script-step', () => { describe('run script step', () => {
beforeEach(async () => { beforeEach(async () => {
testSetup = new TestSetup() testSetup = new TestSetup()
testSetup.initialize() testSetup.initialize()
definitions = {
prepareJob: testSetup.getPrepareJobDefinition(),
runScriptStep: testSetup.getRunScriptStepDefinition()
}
const prepareJobOutput = testSetup.createOutputFile( const prepareJobOutput = testSetup.createOutputFile(
'prepare-job-output.json' 'prepare-job-output.json'
) )
definitions.prepareJob.args.container.registry = null
definitions.prepareJob.args.services.forEach(s => {
s.registry = null
})
await prepareJob(definitions.prepareJob.args, prepareJobOutput) await prepareJob(definitions.prepareJob.args, prepareJobOutput)
prepareJobResponse = JSON.parse(fs.readFileSync(prepareJobOutput, 'utf-8')) prepareJobResponse = JSON.parse(fs.readFileSync(prepareJobOutput, 'utf-8'))

View File

@@ -1,5 +1,6 @@
import * as fs from 'fs' import * as fs from 'fs'
import { Mount } from 'hooklib' import { Mount } from 'hooklib'
import { HookData } from 'hooklib/lib'
import * as path from 'path' import * as path from 'path'
import { env } from 'process' import { env } from 'process'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@@ -51,13 +52,18 @@ export default class TestSetup {
for (const dir of this.allTestDirectories) { for (const dir of this.allTestDirectories) {
fs.mkdirSync(dir, { recursive: true }) fs.mkdirSync(dir, { recursive: true })
} }
fs.copyFileSync(
path.resolve(`${__dirname}/../../../examples/example-script.sh`),
`${env.RUNNER_TEMP}/example-script.sh`
)
} }
public teardown(): void { public teardown(): void {
fs.rmdirSync(this.testdir, { recursive: true }) fs.rmdirSync(this.testdir, { recursive: true })
} }
public get systemMountVolumes(): Mount[] { private get systemMountVolumes(): Mount[] {
return [ return [
{ {
sourceVolumePath: '/var/run/docker.sock', sourceVolumePath: '/var/run/docker.sock',
@@ -140,4 +146,51 @@ echo "::set-output name=time::$time"`
fs.writeFileSync(entryPointPath, content) fs.writeFileSync(entryPointPath, content)
fs.chmodSync(entryPointPath, 0o755) fs.chmodSync(entryPointPath, 0o755)
} }
public getPrepareJobDefinition(): HookData {
const prepareJob = JSON.parse(
fs.readFileSync(
path.resolve(__dirname + '/../../../examples/prepare-job.json'),
'utf8'
)
)
prepareJob.args.container.systemMountVolumes = this.systemMountVolumes
prepareJob.args.container.workingDirectory = this.workingDirectory
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.systemMountVolumes = this.systemMountVolumes
runContainerStep.args.workingDirectory = this.workingDirectory
runContainerStep.args.userMountVolumes = undefined
runContainerStep.args.registry = null
return runContainerStep
}
} }

View File

@@ -100,8 +100,13 @@ function generateResponseFile(
appPod: k8s.V1Pod, appPod: k8s.V1Pod,
isAlpine isAlpine
): void { ): void {
if (!appPod.metadata?.name) {
throw new Error('app pod must have metadata.name specified')
}
const response = { const response = {
state: {}, state: {
jobPod: appPod.metadata.name
},
context: {}, context: {},
isAlpine isAlpine
} }
@@ -163,13 +168,11 @@ function createPodSpec(
name: string, name: string,
jobContainer = false jobContainer = false
): k8s.V1Container { ): k8s.V1Container {
if (!container.entryPointArgs) {
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
}
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
if (!container.entryPoint) { if (!container.entryPoint) {
container.entryPoint = DEFAULT_CONTAINER_ENTRY_POINT container.entryPoint = DEFAULT_CONTAINER_ENTRY_POINT
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
} }
const podContainer = { const podContainer = {
name, name,
image: container.image, image: container.image,

View File

@@ -1,5 +1,5 @@
import * as k8s from '@kubernetes/client-node'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as k8s from '@kubernetes/client-node'
import { RunContainerStepArgs } from 'hooklib' import { RunContainerStepArgs } from 'hooklib'
import { import {
createJob, createJob,
@@ -10,8 +10,14 @@ import {
waitForJobToComplete, waitForJobToComplete,
waitForPodPhases waitForPodPhases
} from '../k8s' } from '../k8s'
import {
containerVolumes,
DEFAULT_CONTAINER_ENTRY_POINT,
DEFAULT_CONTAINER_ENTRY_POINT_ARGS,
PodPhase,
writeEntryPointScript
} from '../k8s/utils'
import { JOB_CONTAINER_NAME } from './constants' import { JOB_CONTAINER_NAME } from './constants'
import { containerVolumes, PodPhase } from '../k8s/utils'
export async function runContainerStep( export async function runContainerStep(
stepContainer: RunContainerStepArgs stepContainer: RunContainerStepArgs
@@ -19,13 +25,16 @@ export async function runContainerStep(
if (stepContainer.dockerfile) { if (stepContainer.dockerfile) {
throw new Error('Building container actions is not currently supported') throw new Error('Building container actions is not currently supported')
} }
let secretName: string | undefined = undefined let secretName: string | undefined = undefined
core.debug('') core.debug('')
if (stepContainer.environmentVariables) { if (stepContainer.environmentVariables) {
secretName = await createSecretForEnvs(stepContainer.environmentVariables) secretName = await createSecretForEnvs(stepContainer.environmentVariables)
} }
core.debug(`Created secret ${secretName} for container job envs`) core.debug(`Created secret ${secretName} for container job envs`)
const container = createPodSpec(stepContainer, secretName) const container = createPodSpec(stepContainer, secretName)
const job = await createJob(container) const job = await createJob(container)
if (!job.metadata?.name) { if (!job.metadata?.name) {
throw new Error( throw new Error(
@@ -35,6 +44,7 @@ export async function runContainerStep(
) )
} }
core.debug(`Job created, waiting for pod to start: ${job.metadata?.name}`) core.debug(`Job created, waiting for pod to start: ${job.metadata?.name}`)
const podName = await getContainerJobPodName(job.metadata.name) const podName = await getContainerJobPodName(job.metadata.name)
await waitForPodPhases( await waitForPodPhases(
podName, podName,
@@ -42,11 +52,16 @@ export async function runContainerStep(
new Set([PodPhase.PENDING, PodPhase.UNKNOWN]) new Set([PodPhase.PENDING, PodPhase.UNKNOWN])
) )
core.debug('Container step is running or complete, pulling logs') core.debug('Container step is running or complete, pulling logs')
await getPodLogs(podName, JOB_CONTAINER_NAME) await getPodLogs(podName, JOB_CONTAINER_NAME)
core.debug('Waiting for container job to complete') core.debug('Waiting for container job to complete')
await waitForJobToComplete(job.metadata.name) await waitForJobToComplete(job.metadata.name)
// pod has failed so pull the status code from the container // pod has failed so pull the status code from the container
const status = await getPodStatus(podName) const status = await getPodStatus(podName)
if (status?.phase === 'Succeeded') {
return 0
}
if (!status?.containerStatuses?.length) { if (!status?.containerStatuses?.length) {
core.error( core.error(
`Can't determine container status from response: ${JSON.stringify( `Can't determine container status from response: ${JSON.stringify(
@@ -68,9 +83,18 @@ function createPodSpec(
const podContainer = new k8s.V1Container() const podContainer = new k8s.V1Container()
podContainer.name = JOB_CONTAINER_NAME podContainer.name = JOB_CONTAINER_NAME
podContainer.image = container.image podContainer.image = container.image
if (container.entryPoint) {
podContainer.command = [container.entryPoint, ...container.entryPointArgs] const { entryPoint, entryPointArgs } = container
} container.entryPoint = 'sh'
const { containerPath } = writeEntryPointScript(
container.workingDirectory,
entryPoint || DEFAULT_CONTAINER_ENTRY_POINT,
entryPoint ? entryPointArgs || [] : DEFAULT_CONTAINER_ENTRY_POINT_ARGS
)
container.entryPointArgs = ['-e', containerPath]
podContainer.command = [container.entryPoint, ...container.entryPointArgs]
if (secretName) { if (secretName) {
podContainer.envFrom = [ podContainer.envFrom = [
{ {
@@ -81,7 +105,7 @@ function createPodSpec(
} }
] ]
} }
podContainer.volumeMounts = containerVolumes() podContainer.volumeMounts = containerVolumes(undefined, false, true)
return podContainer return podContainer
} }

View File

@@ -1,38 +1,35 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import * as fs from 'fs'
import { RunScriptStepArgs } from 'hooklib' import { RunScriptStepArgs } from 'hooklib'
import { execPodStep } from '../k8s' import { execPodStep } from '../k8s'
import { getJobPodName, JOB_CONTAINER_NAME } from './constants' import { writeEntryPointScript } from '../k8s/utils'
import { JOB_CONTAINER_NAME } from './constants'
export async function runScriptStep( export async function runScriptStep(
args: RunScriptStepArgs, args: RunScriptStepArgs,
state, state,
responseFile responseFile
): Promise<void> { ): Promise<void> {
const cb = new CommandsBuilder( const { entryPoint, entryPointArgs, environmentVariables } = args
args.entryPoint, const { containerPath, runnerPath } = writeEntryPointScript(
args.entryPointArgs, args.workingDirectory,
args.environmentVariables entryPoint,
entryPointArgs,
args.prependPath,
environmentVariables
) )
await execPodStep(cb.command, getJobPodName(), JOB_CONTAINER_NAME)
}
class CommandsBuilder { args.entryPoint = 'sh'
constructor( args.entryPointArgs = ['-e', containerPath]
private entryPoint: string, try {
private entryPointArgs: string[], await execPodStep(
private environmentVariables: { [key: string]: string } [args.entryPoint, ...args.entryPointArgs],
) {} state.jobPod,
JOB_CONTAINER_NAME
get command(): string[] { )
const envCommands: string[] = [] } catch (err) {
if ( throw new Error(`failed to run script step: ${JSON.stringify(err)}`)
this.environmentVariables && } finally {
Object.entries(this.environmentVariables).length fs.rmSync(runnerPath)
) {
for (const [key, value] of Object.entries(this.environmentVariables)) {
envCommands.push(`${key}=${value}`)
}
}
return ['env', ...envCommands, this.entryPoint, ...this.entryPointArgs]
} }
} }

View File

@@ -190,12 +190,8 @@ export async function execPodStep(
containerName: string, containerName: string,
stdin?: stream.Readable stdin?: stream.Readable
): Promise<void> { ): Promise<void> {
// TODO, we need to add the path from `prependPath` to the PATH variable. How can we do that? Maybe another exec before running this one?
// Maybe something like, get the current path, if these entries aren't in it, add them, then set the current path to that?
// TODO: how do we set working directory? There doesn't seem to be an easy way to do it. Should we cd then execute our bash script?
const exec = new k8s.Exec(kc) const exec = new k8s.Exec(kc)
return new Promise(async function (resolve, reject) { await new Promise(async function (resolve, reject) {
try { try {
await exec.exec( await exec.exec(
namespace(), namespace(),
@@ -209,16 +205,19 @@ export async function execPodStep(
resp => { resp => {
// kube.exec returns an error if exit code is not 0, but we can't actually get the exit code // kube.exec returns an error if exit code is not 0, but we can't actually get the exit code
if (resp.status === 'Success') { if (resp.status === 'Success') {
resolve() resolve(resp.code)
} else { } else {
reject( reject(
JSON.stringify({ message: resp?.message, details: resp?.details }) JSON.stringify({
message: resp?.message,
details: resp?.details
})
) )
} }
} }
) )
} catch (error) { } catch (error) {
reject(error) reject(JSON.stringify(error))
} }
}) })
} }

View File

@@ -1,6 +1,8 @@
import * as k8s from '@kubernetes/client-node' import * as k8s from '@kubernetes/client-node'
import * as fs from 'fs'
import { Mount } from 'hooklib' import { Mount } from 'hooklib'
import * as path from 'path' import * as path from 'path'
import { v1 as uuidv4 } from 'uuid'
import { POD_VOLUME_NAME } from './index' import { POD_VOLUME_NAME } from './index'
export const DEFAULT_CONTAINER_ENTRY_POINT_ARGS = [`-f`, `/dev/null`] export const DEFAULT_CONTAINER_ENTRY_POINT_ARGS = [`-f`, `/dev/null`]
@@ -8,7 +10,8 @@ export const DEFAULT_CONTAINER_ENTRY_POINT = 'tail'
export function containerVolumes( export function containerVolumes(
userMountVolumes: Mount[] = [], userMountVolumes: Mount[] = [],
jobContainer = true jobContainer = true,
containerAction = false
): k8s.V1VolumeMount[] { ): k8s.V1VolumeMount[] {
const mounts: k8s.V1VolumeMount[] = [ const mounts: k8s.V1VolumeMount[] = [
{ {
@@ -17,6 +20,23 @@ export function containerVolumes(
} }
] ]
if (containerAction) {
const workspace = process.env.GITHUB_WORKSPACE as string
mounts.push(
{
name: POD_VOLUME_NAME,
mountPath: '/github/workspace',
subPath: workspace.substring(workspace.indexOf('work/') + 1)
},
{
name: POD_VOLUME_NAME,
mountPath: '/github/file_commands',
subPath: workspace.substring(workspace.indexOf('work/') + 1)
}
)
return mounts
}
if (!jobContainer) { if (!jobContainer) {
return mounts return mounts
} }
@@ -71,6 +91,48 @@ export function containerVolumes(
return mounts return mounts
} }
export function writeEntryPointScript(
workingDirectory: string,
entryPoint: string,
entryPointArgs?: string[],
prependPath?: string[],
environmentVariables?: { [key: string]: string }
): { containerPath: string; runnerPath: string } {
let exportPath = ''
if (prependPath?.length) {
exportPath = `export PATH=${prependPath.join(':')}:$PATH`
}
let environmentPrefix = ''
if (environmentVariables && Object.entries(environmentVariables).length) {
const envBuffer: string[] = []
for (const [key, value] of Object.entries(environmentVariables)) {
envBuffer.push(
`"${key}=${value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/=/g, '\\=')}"`
)
}
environmentPrefix = `env ${envBuffer.join(' ')} `
}
const content = `#!/bin/sh -l
${exportPath}
cd ${workingDirectory} && \
exec ${environmentPrefix} ${entryPoint} ${
entryPointArgs?.length ? entryPointArgs.join(' ') : ''
}
`
const filename = `${uuidv4()}.sh`
const entryPointPath = `${process.env.RUNNER_TEMP}/${filename}`
fs.writeFileSync(entryPointPath, content)
return {
containerPath: `/__w/_temp/${filename}`,
runnerPath: entryPointPath
}
}
export enum PodPhase { export enum PodPhase {
PENDING = 'Pending', PENDING = 'Pending',
RUNNING = 'Running', RUNNING = 'Running',

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
import * as fs from 'fs'
import * as path from 'path'
import { runContainerStep } from '../src/hooks' import { runContainerStep } from '../src/hooks'
import { TestHelper } from './test-setup' import { TestHelper } from './test-setup'
@@ -7,25 +5,37 @@ jest.useRealTimers()
let testHelper: TestHelper let testHelper: TestHelper
let runContainerStepJsonPath = path.resolve(
`${__dirname}/../../../examples/run-container-step.json`
)
let runContainerStepData: any let runContainerStepData: any
describe('Run container step', () => { describe('Run container step', () => {
beforeAll(async () => { beforeEach(async () => {
const content = fs.readFileSync(runContainerStepJsonPath)
runContainerStepData = JSON.parse(content.toString())
testHelper = new TestHelper() testHelper = new TestHelper()
await testHelper.initialize() await testHelper.initialize()
runContainerStepData = testHelper.getRunContainerStepDefinition()
}) })
afterEach(async () => {
await testHelper.cleanup()
})
it('should not throw', async () => { 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( await expect(
runContainerStep(runContainerStepData.args) runContainerStep(runContainerStepData.args)
).resolves.not.toThrow() ).resolves.not.toThrow()
}) })
afterEach(async () => {
await testHelper.cleanup()
})
}) })

View File

@@ -1,5 +1,4 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path'
import { cleanupJob, prepareJob, runScriptStep } from '../src/hooks' import { cleanupJob, prepareJob, runScriptStep } from '../src/hooks'
import { TestHelper } from './test-setup' import { TestHelper } from './test-setup'
@@ -7,22 +6,21 @@ jest.useRealTimers()
let testHelper: TestHelper let testHelper: TestHelper
const prepareJobJsonPath = path.resolve(
`${__dirname}/../../../examples/prepare-job.json`
)
let prepareJobData: any
let prepareJobOutputFilePath: string
let prepareJobOutputData: any let prepareJobOutputData: any
let runScriptStepDefinition
describe('Run script step', () => { describe('Run script step', () => {
beforeEach(async () => { beforeEach(async () => {
const prepareJobJson = fs.readFileSync(prepareJobJsonPath)
prepareJobData = JSON.parse(prepareJobJson.toString())
testHelper = new TestHelper() testHelper = new TestHelper()
await testHelper.initialize() 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) await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
const outputContent = fs.readFileSync(prepareJobOutputFilePath) const outputContent = fs.readFileSync(prepareJobOutputFilePath)
prepareJobOutputData = JSON.parse(outputContent.toString()) prepareJobOutputData = JSON.parse(outputContent.toString())
@@ -38,21 +36,39 @@ describe('Run script step', () => {
// npm run test run-script-step // npm run test run-script-step
it('should not throw an exception', async () => { 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( 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() ).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 k8s from '@kubernetes/client-node'
import * as fs from 'fs' import * as fs from 'fs'
import { HookData } from 'hooklib/lib'
import * as path from 'path'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
const kc = new k8s.KubeConfig() const kc = new k8s.KubeConfig()
@@ -20,17 +22,27 @@ export class TestHelper {
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
process.env['ACTIONS_RUNNER_POD_NAME'] = `${this.podName}` process.env['ACTIONS_RUNNER_POD_NAME'] = `${this.podName}`
process.env['ACTIONS_RUNNER_CLAIM_NAME'] = `${this.podName}-work` process.env['ACTIONS_RUNNER_CLAIM_NAME'] = `${this.podName}-work`
process.env['RUNNER_WORKSPACE'] = `${this.tempDirPath}/work/repo` process.env['RUNNER_WORKSPACE'] = `${this.tempDirPath}/_work/repo`
process.env['GITHUB_WORKSPACE'] = `${this.tempDirPath}/work/repo/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' 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() await this.cleanupK8sResources()
try { try {
await this.createTestVolume() await this.createTestVolume()
await this.createTestJobPod() await this.createTestJobPod()
} catch {} } catch (e) {
fs.mkdirSync(`${this.tempDirPath}/work/repo/repo`, { recursive: true }) console.log(JSON.stringify(e))
fs.mkdirSync(`${this.tempDirPath}/externals`, { recursive: true }) }
} }
public async cleanup(): Promise<void> { public async cleanup(): Promise<void> {
@@ -116,7 +128,7 @@ export class TestHelper {
volumeMode: 'Filesystem', volumeMode: 'Filesystem',
accessModes: ['ReadWriteOnce'], accessModes: ['ReadWriteOnce'],
hostPath: { hostPath: {
path: this.tempDirPath path: `${this.tempDirPath}/_work`
} }
} }
} }
@@ -139,4 +151,47 @@ export class TestHelper {
} }
await k8sApi.createNamespacedPersistentVolumeClaim('default', volumeClaim) 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
}
} }