From 8ea57170d8827fec1cdb71efffb453b952a4d0e3 Mon Sep 17 00:00:00 2001 From: Nikola Jokic <97525037+nikola-jokic@users.noreply.github.com> Date: Wed, 15 Jun 2022 03:41:49 +0200 Subject: [PATCH] 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 Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com> --- .github/workflows/build.yaml | 8 ++- .gitignore | 3 +- examples/example-script.sh | 3 + examples/run-container-step.json | 4 +- examples/run-script-step.json | 4 +- packages/docker/tests/cleanup-job-test.ts | 18 ++--- packages/docker/tests/container-build-test.ts | 14 ++-- packages/docker/tests/e2e-test.ts | 36 ++-------- packages/docker/tests/prepare-job-test.ts | 14 +--- ...script-step.ts => run-script-step-test.ts} | 28 ++------ packages/docker/tests/test-setup.ts | 55 ++++++++++++++- packages/k8s/src/hooks/prepare-job.ts | 13 ++-- packages/k8s/src/hooks/run-container-step.ts | 36 ++++++++-- packages/k8s/src/hooks/run-script-step.ts | 47 ++++++------- packages/k8s/src/k8s/index.ts | 15 ++--- packages/k8s/src/k8s/utils.ts | 64 +++++++++++++++++- packages/k8s/tests/cleanup-job-test.ts | 18 ++--- packages/k8s/tests/e2e-test.ts | 22 ++---- packages/k8s/tests/prepare-job-test.ts | 34 +++++----- packages/k8s/tests/run-container-step-test.ts | 34 ++++++---- packages/k8s/tests/run-script-step-test.ts | 66 +++++++++++------- packages/k8s/tests/test-kind.yaml | 18 +++++ packages/k8s/tests/test-setup.ts | 67 +++++++++++++++++-- 23 files changed, 391 insertions(+), 230 deletions(-) create mode 100644 examples/example-script.sh rename packages/docker/tests/{run-script-step.ts => run-script-step-test.ts} (60%) create mode 100644 packages/k8s/tests/test-kind.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e3b1580..dded697 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,8 +10,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: helm/kind-action@v1.2.0 - 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 name: Install dependencies - run: npm run bootstrap @@ -22,6 +26,6 @@ jobs: - name: Check linter run: | npm run lint - git diff --exit-code + git diff --exit-code -- ':!packages/k8s/tests/test-kind.yaml' - name: Run tests run: npm run test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 15fa2de..a48fc2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ lib/ dist/ -**/tests/_temp/** \ No newline at end of file +**/tests/_temp/** +packages/k8s/tests/test-kind.yaml \ No newline at end of file diff --git a/examples/example-script.sh b/examples/example-script.sh new file mode 100644 index 0000000..283c8e8 --- /dev/null +++ b/examples/example-script.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "Hello World" \ No newline at end of file diff --git a/examples/run-container-step.json b/examples/run-container-step.json index 5e13f6e..8c15d4e 100644 --- a/examples/run-container-step.json +++ b/examples/run-container-step.json @@ -12,8 +12,8 @@ "image": "node:14.16", "dockerfile": null, "entryPointArgs": [ - "-c", - "echo \"hello world2\"" + "-e", + "example-script.sh" ], "entryPoint": "bash", "workingDirectory": "/__w/repo/repo", diff --git a/examples/run-script-step.json b/examples/run-script-step.json index 1b57dc1..e7e943b 100644 --- a/examples/run-script-step.json +++ b/examples/run-script-step.json @@ -10,8 +10,8 @@ }, "args": { "entryPointArgs": [ - "-c", - "echo \"hello world\"" + "-e", + "example-script.sh" ], "entryPoint": "bash", "environmentVariables": { diff --git a/packages/docker/tests/cleanup-job-test.ts b/packages/docker/tests/cleanup-job-test.ts index 6847ebe..a26926c 100644 --- a/packages/docker/tests/cleanup-job-test.ts +++ b/packages/docker/tests/cleanup-job-test.ts @@ -1,4 +1,4 @@ -import * as fs from 'fs' +import { PrepareJobArgs } from 'hooklib/lib' import { cleanupJob, prepareJob } from '../src/hooks' import TestSetup from './test-setup' @@ -11,22 +11,16 @@ describe('cleanup job', () => { testSetup = new TestSetup() testSetup.initialize() - const prepareJobDefinition = JSON.parse( - fs.readFileSync( - `${__dirname}/../../../examples/prepare-job.json`, - 'utf-8' - ) - ) + const prepareJobDefinition = testSetup.getPrepareJobDefinition() const prepareJobOutput = testSetup.createOutputFile( 'prepare-job-output.json' ) - prepareJobDefinition.args.container.registry = null - prepareJobDefinition.args.services.forEach(s => { - s.registry = null - }) - await prepareJob(prepareJobDefinition.args, prepareJobOutput) + await prepareJob( + prepareJobDefinition.args as PrepareJobArgs, + prepareJobOutput + ) }) afterEach(() => { diff --git a/packages/docker/tests/container-build-test.ts b/packages/docker/tests/container-build-test.ts index 8b0150c..b575e29 100644 --- a/packages/docker/tests/container-build-test.ts +++ b/packages/docker/tests/container-build-test.ts @@ -1,24 +1,15 @@ -import * as fs from 'fs' import { containerBuild } from '../src/dockerCommands' import TestSetup from './test-setup' let testSetup let runContainerStepDefinition -const runContainerStepInputPath = `${__dirname}/../../../examples/run-container-step.json` describe('container build', () => { beforeEach(() => { testSetup = new TestSetup() testSetup.initialize() - let runContainerStepJson = fs.readFileSync( - runContainerStepInputPath, - 'utf8' - ) - runContainerStepDefinition = JSON.parse(runContainerStepJson.toString()) - runContainerStepDefinition.image = '' - const actionPath = testSetup.initializeDockerAction() - runContainerStepDefinition.dockerfile = `${actionPath}/Dockerfile` + runContainerStepDefinition = testSetup.getRunContainerStepDefinition() }) afterEach(() => { @@ -26,6 +17,9 @@ describe('container build', () => { }) it('should build container', async () => { + runContainerStepDefinition.image = '' + const actionPath = testSetup.initializeDockerAction() + runContainerStepDefinition.dockerfile = `${actionPath}/Dockerfile` await expect( containerBuild(runContainerStepDefinition, 'example-test-tag') ).resolves.not.toThrow() diff --git a/packages/docker/tests/e2e-test.ts b/packages/docker/tests/e2e-test.ts index 67f63d9..62e0aa2 100644 --- a/packages/docker/tests/e2e-test.ts +++ b/packages/docker/tests/e2e-test.ts @@ -1,5 +1,4 @@ import * as fs from 'fs' -import * as path from 'path' import { cleanupJob, prepareJob, @@ -8,28 +7,7 @@ import { } from '../src/hooks' import TestSetup from './test-setup' -const 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 definitions let testSetup: TestSetup @@ -37,12 +15,12 @@ describe('e2e', () => { beforeEach(() => { testSetup = new TestSetup() testSetup.initialize() - definitions.prepareJob.args.container.systemMountVolumes = - testSetup.systemMountVolumes - definitions.prepareJob.args.container.registry = null - definitions.prepareJob.args.services.forEach(s => { - s.registry = null - }) + + definitions = { + prepareJob: testSetup.getPrepareJobDefinition(), + runScriptStep: testSetup.getRunScriptStepDefinition(), + runContainerStep: testSetup.getRunContainerStepDefinition() + } }) afterEach(() => { diff --git a/packages/docker/tests/prepare-job-test.ts b/packages/docker/tests/prepare-job-test.ts index d0d80f6..6076197 100644 --- a/packages/docker/tests/prepare-job-test.ts +++ b/packages/docker/tests/prepare-job-test.ts @@ -4,9 +4,7 @@ import TestSetup from './test-setup' jest.useRealTimers() -const prepareJobDefinition = JSON.parse( - fs.readFileSync(`${__dirname}/../../../examples/prepare-job.json`, 'utf-8') -) +let prepareJobDefinition let testSetup: TestSetup @@ -14,15 +12,7 @@ describe('prepare job', () => { beforeEach(() => { testSetup = new TestSetup() testSetup.initialize() - - 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 - }) + prepareJobDefinition = testSetup.getPrepareJobDefinition() }) afterEach(() => { diff --git a/packages/docker/tests/run-script-step.ts b/packages/docker/tests/run-script-step-test.ts similarity index 60% rename from packages/docker/tests/run-script-step.ts rename to packages/docker/tests/run-script-step-test.ts index 1dc7cca..429ae3b 100644 --- a/packages/docker/tests/run-script-step.ts +++ b/packages/docker/tests/run-script-step-test.ts @@ -1,6 +1,5 @@ import * as fs from 'fs' import { PrepareJobResponse } from 'hooklib/lib' -import * as path from 'path' import { prepareJob, runScriptStep } from '../src/hooks' import TestSetup from './test-setup' @@ -8,36 +7,23 @@ jest.useRealTimers() let testSetup: TestSetup -const 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 definitions let prepareJobResponse: PrepareJobResponse -describe('run-script-step', () => { +describe('run script step', () => { beforeEach(async () => { testSetup = new TestSetup() testSetup.initialize() + definitions = { + prepareJob: testSetup.getPrepareJobDefinition(), + runScriptStep: testSetup.getRunScriptStepDefinition() + } + const prepareJobOutput = testSetup.createOutputFile( '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) prepareJobResponse = JSON.parse(fs.readFileSync(prepareJobOutput, 'utf-8')) diff --git a/packages/docker/tests/test-setup.ts b/packages/docker/tests/test-setup.ts index 011631f..2100797 100644 --- a/packages/docker/tests/test-setup.ts +++ b/packages/docker/tests/test-setup.ts @@ -1,5 +1,6 @@ import * as fs from 'fs' import { Mount } from 'hooklib' +import { HookData } from 'hooklib/lib' import * as path from 'path' import { env } from 'process' import { v4 as uuidv4 } from 'uuid' @@ -51,13 +52,18 @@ export default class TestSetup { for (const dir of this.allTestDirectories) { fs.mkdirSync(dir, { recursive: true }) } + + fs.copyFileSync( + path.resolve(`${__dirname}/../../../examples/example-script.sh`), + `${env.RUNNER_TEMP}/example-script.sh` + ) } public teardown(): void { fs.rmdirSync(this.testdir, { recursive: true }) } - public get systemMountVolumes(): Mount[] { + private get systemMountVolumes(): Mount[] { return [ { sourceVolumePath: '/var/run/docker.sock', @@ -140,4 +146,51 @@ echo "::set-output name=time::$time"` fs.writeFileSync(entryPointPath, content) 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 + } } diff --git a/packages/k8s/src/hooks/prepare-job.ts b/packages/k8s/src/hooks/prepare-job.ts index e5ecf6a..9f9beba 100644 --- a/packages/k8s/src/hooks/prepare-job.ts +++ b/packages/k8s/src/hooks/prepare-job.ts @@ -100,8 +100,13 @@ function generateResponseFile( appPod: k8s.V1Pod, isAlpine ): void { + if (!appPod.metadata?.name) { + throw new Error('app pod must have metadata.name specified') + } const response = { - state: {}, + state: { + jobPod: appPod.metadata.name + }, context: {}, isAlpine } @@ -163,13 +168,11 @@ function createPodSpec( name: string, jobContainer = false ): k8s.V1Container { - if (!container.entryPointArgs) { - container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS - } - container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS if (!container.entryPoint) { container.entryPoint = DEFAULT_CONTAINER_ENTRY_POINT + container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS } + const podContainer = { name, image: container.image, diff --git a/packages/k8s/src/hooks/run-container-step.ts b/packages/k8s/src/hooks/run-container-step.ts index 73594e0..adaf6b6 100644 --- a/packages/k8s/src/hooks/run-container-step.ts +++ b/packages/k8s/src/hooks/run-container-step.ts @@ -1,5 +1,5 @@ -import * as k8s from '@kubernetes/client-node' import * as core from '@actions/core' +import * as k8s from '@kubernetes/client-node' import { RunContainerStepArgs } from 'hooklib' import { createJob, @@ -10,8 +10,14 @@ import { waitForJobToComplete, waitForPodPhases } 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 { containerVolumes, PodPhase } from '../k8s/utils' export async function runContainerStep( stepContainer: RunContainerStepArgs @@ -19,13 +25,16 @@ export async function runContainerStep( if (stepContainer.dockerfile) { throw new Error('Building container actions is not currently supported') } + let secretName: string | undefined = undefined core.debug('') if (stepContainer.environmentVariables) { secretName = await createSecretForEnvs(stepContainer.environmentVariables) } + core.debug(`Created secret ${secretName} for container job envs`) const container = createPodSpec(stepContainer, secretName) + const job = await createJob(container) if (!job.metadata?.name) { throw new Error( @@ -35,6 +44,7 @@ export async function runContainerStep( ) } core.debug(`Job created, waiting for pod to start: ${job.metadata?.name}`) + const podName = await getContainerJobPodName(job.metadata.name) await waitForPodPhases( podName, @@ -42,11 +52,16 @@ export async function runContainerStep( new Set([PodPhase.PENDING, PodPhase.UNKNOWN]) ) core.debug('Container step is running or complete, pulling logs') + await getPodLogs(podName, JOB_CONTAINER_NAME) + core.debug('Waiting for container job to complete') await waitForJobToComplete(job.metadata.name) // pod has failed so pull the status code from the container const status = await getPodStatus(podName) + if (status?.phase === 'Succeeded') { + return 0 + } if (!status?.containerStatuses?.length) { core.error( `Can't determine container status from response: ${JSON.stringify( @@ -68,9 +83,18 @@ function createPodSpec( const podContainer = new k8s.V1Container() podContainer.name = JOB_CONTAINER_NAME 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) { podContainer.envFrom = [ { @@ -81,7 +105,7 @@ function createPodSpec( } ] } - podContainer.volumeMounts = containerVolumes() + podContainer.volumeMounts = containerVolumes(undefined, false, true) return podContainer } diff --git a/packages/k8s/src/hooks/run-script-step.ts b/packages/k8s/src/hooks/run-script-step.ts index e00c209..64ff1d7 100644 --- a/packages/k8s/src/hooks/run-script-step.ts +++ b/packages/k8s/src/hooks/run-script-step.ts @@ -1,38 +1,35 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import * as fs from 'fs' import { RunScriptStepArgs } from 'hooklib' 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( args: RunScriptStepArgs, state, responseFile ): Promise { - const cb = new CommandsBuilder( - args.entryPoint, - args.entryPointArgs, - args.environmentVariables + const { entryPoint, entryPointArgs, environmentVariables } = args + const { containerPath, runnerPath } = writeEntryPointScript( + args.workingDirectory, + entryPoint, + entryPointArgs, + args.prependPath, + environmentVariables ) - await execPodStep(cb.command, getJobPodName(), JOB_CONTAINER_NAME) -} -class CommandsBuilder { - constructor( - private entryPoint: string, - private entryPointArgs: string[], - private environmentVariables: { [key: string]: string } - ) {} - - get command(): string[] { - const envCommands: string[] = [] - if ( - this.environmentVariables && - Object.entries(this.environmentVariables).length - ) { - for (const [key, value] of Object.entries(this.environmentVariables)) { - envCommands.push(`${key}=${value}`) - } - } - return ['env', ...envCommands, this.entryPoint, ...this.entryPointArgs] + args.entryPoint = 'sh' + args.entryPointArgs = ['-e', containerPath] + try { + await execPodStep( + [args.entryPoint, ...args.entryPointArgs], + state.jobPod, + JOB_CONTAINER_NAME + ) + } catch (err) { + throw new Error(`failed to run script step: ${JSON.stringify(err)}`) + } finally { + fs.rmSync(runnerPath) } } diff --git a/packages/k8s/src/k8s/index.ts b/packages/k8s/src/k8s/index.ts index e210e94..445c2c8 100644 --- a/packages/k8s/src/k8s/index.ts +++ b/packages/k8s/src/k8s/index.ts @@ -190,12 +190,8 @@ export async function execPodStep( containerName: string, stdin?: stream.Readable ): Promise { - // 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) - return new Promise(async function (resolve, reject) { + await new Promise(async function (resolve, reject) { try { await exec.exec( namespace(), @@ -209,16 +205,19 @@ export async function execPodStep( resp => { // kube.exec returns an error if exit code is not 0, but we can't actually get the exit code if (resp.status === 'Success') { - resolve() + resolve(resp.code) } else { reject( - JSON.stringify({ message: resp?.message, details: resp?.details }) + JSON.stringify({ + message: resp?.message, + details: resp?.details + }) ) } } ) } catch (error) { - reject(error) + reject(JSON.stringify(error)) } }) } diff --git a/packages/k8s/src/k8s/utils.ts b/packages/k8s/src/k8s/utils.ts index a8901c4..2cf7d5f 100644 --- a/packages/k8s/src/k8s/utils.ts +++ b/packages/k8s/src/k8s/utils.ts @@ -1,6 +1,8 @@ import * as k8s from '@kubernetes/client-node' +import * as fs from 'fs' import { Mount } from 'hooklib' import * as path from 'path' +import { v1 as uuidv4 } from 'uuid' import { POD_VOLUME_NAME } from './index' export const DEFAULT_CONTAINER_ENTRY_POINT_ARGS = [`-f`, `/dev/null`] @@ -8,7 +10,8 @@ export const DEFAULT_CONTAINER_ENTRY_POINT = 'tail' export function containerVolumes( userMountVolumes: Mount[] = [], - jobContainer = true + jobContainer = true, + containerAction = false ): 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) { return mounts } @@ -71,6 +91,48 @@ export function containerVolumes( 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 { PENDING = 'Pending', RUNNING = 'Running', diff --git a/packages/k8s/tests/cleanup-job-test.ts b/packages/k8s/tests/cleanup-job-test.ts index 7a5d8c8..0b50a3c 100644 --- a/packages/k8s/tests/cleanup-job-test.ts +++ b/packages/k8s/tests/cleanup-job-test.ts @@ -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 () => { diff --git a/packages/k8s/tests/e2e-test.ts b/packages/k8s/tests/e2e-test.ts index 458eaa3..5b73bf8 100644 --- a/packages/k8s/tests/e2e-test.ts +++ b/packages/k8s/tests/e2e-test.ts @@ -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) diff --git a/packages/k8s/tests/prepare-job-test.ts b/packages/k8s/tests/prepare-job-test.ts index 00a5759..1bb6067 100644 --- a/packages/k8s/tests/prepare-job-test.ts +++ b/packages/k8s/tests/prepare-job-test.ts @@ -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() diff --git a/packages/k8s/tests/run-container-step-test.ts b/packages/k8s/tests/run-container-step-test.ts index a28512a..108131b 100644 --- a/packages/k8s/tests/run-container-step-test.ts +++ b/packages/k8s/tests/run-container-step-test.ts @@ -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() - }) }) diff --git a/packages/k8s/tests/run-script-step-test.ts b/packages/k8s/tests/run-script-step-test.ts index dfa6edb..a565b4f 100644 --- a/packages/k8s/tests/run-script-step-test.ts +++ b/packages/k8s/tests/run-script-step-test.ts @@ -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() }) }) diff --git a/packages/k8s/tests/test-kind.yaml b/packages/k8s/tests/test-kind.yaml new file mode 100644 index 0000000..a711d70 --- /dev/null +++ b/packages/k8s/tests/test-kind.yaml @@ -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 diff --git a/packages/k8s/tests/test-setup.ts b/packages/k8s/tests/test-setup.ts index 40d7ebb..150abcb 100644 --- a/packages/k8s/tests/test-setup.ts +++ b/packages/k8s/tests/test-setup.ts @@ -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 { 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 { @@ -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 + } }