From 4b7efe88ef4a47acb02ca8f88bfcca4fe4bf211a Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Fri, 3 Jun 2022 14:10:15 +0200 Subject: [PATCH] refactored tests and added docker build test, repaired state.network --- .../docker/src/dockerCommands/container.ts | 41 ++++++++-- .../docker/src/hooks/run-container-step.ts | 39 +++++----- packages/docker/src/index.ts | 2 + packages/docker/src/utils.ts | 7 ++ packages/docker/tests/cleanup-job-test.ts | 44 +++++------ packages/docker/tests/container-build-test.ts | 33 +++++++++ packages/docker/tests/e2e-test.ts | 61 ++++++++------- packages/docker/tests/prepare-job-test.ts | 16 ++-- packages/docker/tests/test-setup.ts | 74 +++++++++++++------ 9 files changed, 204 insertions(+), 113 deletions(-) create mode 100644 packages/docker/tests/container-build-test.ts diff --git a/packages/docker/src/dockerCommands/container.ts b/packages/docker/src/dockerCommands/container.ts index 9d46166..0a4904e 100644 --- a/packages/docker/src/dockerCommands/container.ts +++ b/packages/docker/src/dockerCommands/container.ts @@ -7,7 +7,6 @@ import { ServiceContainerInfo, StepContainerInfo } from 'hooklib/lib' -import path from 'path' import { env } from 'process' import { v4 as uuidv4 } from 'uuid' import { runDockerCommand, RunDockerCommandOptions } from '../utils' @@ -146,17 +145,43 @@ export async function containerBuild( args: RunContainerStepArgs, tag: string ): Promise { - const context = path.dirname(`${env.GITHUB_WORKSPACE}/${args.dockerfile}`) + if (!args.dockerfile) { + throw new Error('Container build expets args.dockerfile to be set') + } + const dockerArgs: string[] = ['build'] dockerArgs.push('-t', tag) - dockerArgs.push('-f', `${env.GITHUB_WORKSPACE}/${args.dockerfile}`) - dockerArgs.push(context) + dockerArgs.push('-f', args.dockerfile) + dockerArgs.push(getBuildContext(args.dockerfile)) // TODO: figure out build working directory await runDockerCommand(dockerArgs, { - workingDir: args['buildWorkingDirectory'] + workingDir: getWorkingDir(args.dockerfile) }) } +function getBuildContext(dockerfilePath: string): string { + const pathSplit = dockerfilePath.split('/') + pathSplit.splice(pathSplit.length - 1) + return pathSplit.join('/') +} + +function getWorkingDir(dockerfilePath: string): string { + const workspace = env.GITHUB_WORKSPACE as string + let workingDir = workspace + if (!dockerfilePath?.includes(workspace)) { + // This is container action + const pathSplit = dockerfilePath.split('/') + const actionIndex = pathSplit?.findIndex(d => d === '_actions') + if (actionIndex) { + const actionSubdirectoryDepth = 3 // handle + repo + [branch | tag] + pathSplit.splice(actionIndex + actionSubdirectoryDepth + 1) + workingDir = pathSplit.join('/') + } + } + + return workingDir +} + export async function containerLogs(id: string): Promise { const dockerArgs: string[] = ['logs'] dockerArgs.push('--details') @@ -330,7 +355,7 @@ export async function containerExecStep( export async function containerRun( args: RunContainerStepArgs, name: string, - network: string + network?: string ): Promise { if (!args.image) { throw new Error('expected image to be set') @@ -340,7 +365,9 @@ export async function containerRun( dockerArgs.push('--name', name) dockerArgs.push(`--workdir=${args.workingDirectory}`) dockerArgs.push(`--label=${getRunnerLabel()}`) - dockerArgs.push(`--network=${network}`) + if (network) { + dockerArgs.push(`--network=${network}`) + } if (args.createOptions) { dockerArgs.push(...args.createOptions.split(' ')) diff --git a/packages/docker/src/hooks/run-container-step.ts b/packages/docker/src/hooks/run-container-step.ts index 7823288..02555dc 100644 --- a/packages/docker/src/hooks/run-container-step.ts +++ b/packages/docker/src/hooks/run-container-step.ts @@ -1,13 +1,12 @@ +import { RunContainerStepArgs } from 'hooklib/lib' +import { v4 as uuidv4 } from 'uuid' import { containerBuild, - registryLogin, - registryLogout, containerPull, - containerRun + containerRun, + registryLogin, + registryLogout } from '../dockerCommands' -import { v4 as uuidv4 } from 'uuid' -import * as core from '@actions/core' -import { RunContainerStepArgs } from 'hooklib/lib' import { getRunnerLabel } from '../dockerCommands/constants' export async function runContainerStep( @@ -15,23 +14,23 @@ export async function runContainerStep( state ): Promise { const tag = generateBuildTag() // for docker build - if (!args.image) { - core.error('expected an image') - } else { - if (args.dockerfile) { - await containerBuild(args, tag) - args.image = tag - } else { - const configLocation = await registryLogin(args) - try { - await containerPull(args.image, configLocation) - } finally { - await registryLogout(configLocation) - } + if (args.image) { + const configLocation = await registryLogin(args) + try { + await containerPull(args.image, configLocation) + } finally { + await registryLogout(configLocation) } + } else if (args.dockerfile) { + await containerBuild(args, tag) + args.image = tag + } else { + throw new Error( + 'run container step should have image or dockerfile fields specified' + ) } // container will get pruned at the end of the job based on the label, no need to cleanup here - await containerRun(args, tag.split(':')[1], state.network) + await containerRun(args, tag.split(':')[1], state?.network) } function generateBuildTag(): string { diff --git a/packages/docker/src/index.ts b/packages/docker/src/index.ts index c620392..4cb91b7 100644 --- a/packages/docker/src/index.ts +++ b/packages/docker/src/index.ts @@ -13,6 +13,7 @@ import { runContainerStep, runScriptStep } from './hooks' +import { checkEnvironment } from './utils' async function run(): Promise { const input = await getInputFromStdin() @@ -23,6 +24,7 @@ async function run(): Promise { const state = input['state'] try { + checkEnvironment() switch (command) { case Command.PrepareJob: await prepareJob(args as PrepareJobArgs, responseFile) diff --git a/packages/docker/src/utils.ts b/packages/docker/src/utils.ts index b624403..69580fe 100644 --- a/packages/docker/src/utils.ts +++ b/packages/docker/src/utils.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ import * as core from '@actions/core' +import { env } from 'process' // Import this way otherwise typescript has errors const exec = require('@actions/exec') @@ -42,6 +43,12 @@ export function sanitize(val: string): string { return newNameBuilder.join('') } +export function checkEnvironment(): void { + if (!env.GITHUB_WORKSPACE) { + throw new Error('GITHUB_WORKSPACE is not set') + } +} + // isAlpha accepts single character and checks if // that character is [a-zA-Z] function isAlpha(val: string): boolean { diff --git a/packages/docker/tests/cleanup-job-test.ts b/packages/docker/tests/cleanup-job-test.ts index efee5b0..d0946be 100644 --- a/packages/docker/tests/cleanup-job-test.ts +++ b/packages/docker/tests/cleanup-job-test.ts @@ -1,47 +1,41 @@ -import { prepareJob, cleanupJob } from '../src/hooks' -import { v4 as uuidv4 } from 'uuid' import * as fs from 'fs' import * as path from 'path' +import { v4 as uuidv4 } from 'uuid' +import { cleanupJob, prepareJob } from '../src/hooks' import TestSetup from './test-setup' const prepareJobInputPath = path.resolve( `${__dirname}/../../../examples/prepare-job.json` ) -const tmpOutputDir = `${__dirname}/${uuidv4()}` - let prepareJobOutputPath: string -let prepareJobData: any +let prepareJobDefinition: any let testSetup: TestSetup jest.useRealTimers() describe('cleanup job', () => { - beforeAll(() => { - fs.mkdirSync(tmpOutputDir, { recursive: true }) - }) - - afterAll(() => { - fs.rmSync(tmpOutputDir, { recursive: true }) - }) - beforeEach(async () => { - const prepareJobRawData = fs.readFileSync(prepareJobInputPath, 'utf8') - prepareJobData = JSON.parse(prepareJobRawData.toString()) - - prepareJobOutputPath = `${tmpOutputDir}/prepare-job-output-${uuidv4()}.json` - fs.writeFileSync(prepareJobOutputPath, '') - testSetup = new TestSetup() testSetup.initialize() - prepareJobData.args.container.userMountVolumes = testSetup.userMountVolumes - prepareJobData.args.container.systemMountVolumes = - testSetup.systemMountVolumes - prepareJobData.args.container.workingDirectory = testSetup.workingDirectory + const prepareJobRawData = fs.readFileSync(prepareJobInputPath, 'utf8') + prepareJobDefinition = JSON.parse(prepareJobRawData.toString()) - await prepareJob(prepareJobData.args, prepareJobOutputPath) + prepareJobOutputPath = `${ + testSetup.testDir + }/prepare-job-output-${uuidv4()}.json` + fs.writeFileSync(prepareJobOutputPath, '') + + prepareJobDefinition.args.container.userMountVolumes = + testSetup.userMountVolumes + prepareJobDefinition.args.container.systemMountVolumes = + testSetup.systemMountVolumes + prepareJobDefinition.args.container.workingDirectory = + testSetup.containerWorkingDirectory + + await prepareJob(prepareJobDefinition.args, prepareJobOutputPath) }) afterEach(() => { @@ -56,7 +50,7 @@ describe('cleanup job', () => { ) const parsedPrepareJobOutput = JSON.parse(prepareJobOutputContent) await expect( - cleanupJob(prepareJobData.args, parsedPrepareJobOutput.state, null) + cleanupJob(prepareJobDefinition.args, parsedPrepareJobOutput.state, null) ).resolves.not.toThrow() }) }) diff --git a/packages/docker/tests/container-build-test.ts b/packages/docker/tests/container-build-test.ts new file mode 100644 index 0000000..8b0150c --- /dev/null +++ b/packages/docker/tests/container-build-test.ts @@ -0,0 +1,33 @@ +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` + }) + + afterEach(() => { + testSetup.teardown() + }) + + it('should build container', async () => { + 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 cdc889b..4c5e1d7 100644 --- a/packages/docker/tests/e2e-test.ts +++ b/packages/docker/tests/e2e-test.ts @@ -1,12 +1,12 @@ -import { - prepareJob, - cleanupJob, - runScriptStep, - runContainerStep -} from '../src/hooks' import * as fs from 'fs' import * as path from 'path' import { v4 as uuidv4 } from 'uuid' +import { + cleanupJob, + prepareJob, + runContainerStep, + runScriptStep +} from '../src/hooks' import TestSetup from './test-setup' const prepareJobJson = fs.readFileSync( @@ -21,10 +21,9 @@ const containerStepJson = fs.readFileSync( const tmpOutputDir = `${__dirname}/_temp/${uuidv4()}` -let prepareJobData: any -let scriptStepJson: any -let scriptStepData: any -let containerStepData: any +let prepareJobDefinition: any +let scriptStepDefinition: any +let runContainerStepDefinition: any let prepareJobOutputFilePath: string @@ -44,23 +43,29 @@ describe('e2e', () => { testSetup = new TestSetup() testSetup.initialize() - prepareJobData = JSON.parse(prepareJobJson) - prepareJobData.args.container.userMountVolumes = testSetup.userMountVolumes - prepareJobData.args.container.systemMountVolumes = + prepareJobDefinition = JSON.parse(prepareJobJson) + prepareJobDefinition.args.container.userMountVolumes = + testSetup.userMountVolumes + prepareJobDefinition.args.container.systemMountVolumes = testSetup.systemMountVolumes - prepareJobData.args.container.workingDirectory = testSetup.workingDirectory + prepareJobDefinition.args.container.workingDirectory = + testSetup.containerWorkingDirectory - scriptStepJson = fs.readFileSync( + const scriptStepJson = fs.readFileSync( path.resolve(__dirname + '/../../../examples/run-script-step.json'), 'utf8' ) - scriptStepData = JSON.parse(scriptStepJson) - scriptStepData.args.workingDirectory = testSetup.workingDirectory + scriptStepDefinition = JSON.parse(scriptStepJson) + scriptStepDefinition.args.workingDirectory = + testSetup.containerWorkingDirectory - containerStepData = JSON.parse(containerStepJson) - containerStepData.args.workingDirectory = testSetup.workingDirectory - containerStepData.args.userMountVolumes = testSetup.userMountVolumes - containerStepData.args.systemMountVolumes = testSetup.systemMountVolumes + runContainerStepDefinition = JSON.parse(containerStepJson) + runContainerStepDefinition.args.workingDirectory = + testSetup.containerWorkingDirectory + runContainerStepDefinition.args.userMountVolumes = + testSetup.userMountVolumes + runContainerStepDefinition.args.systemMountVolumes = + testSetup.systemMountVolumes prepareJobOutputFilePath = `${tmpOutputDir}/prepare-job-output-${uuidv4()}.json` fs.writeFileSync(prepareJobOutputFilePath, '') @@ -73,27 +78,27 @@ describe('e2e', () => { it('should prepare job, then run script step, then run container step then cleanup', async () => { await expect( - prepareJob(prepareJobData.args, prepareJobOutputFilePath) + prepareJob(prepareJobDefinition.args, prepareJobOutputFilePath) ).resolves.not.toThrow() let rawState = fs.readFileSync(prepareJobOutputFilePath, 'utf-8') let resp = JSON.parse(rawState) await expect( - runScriptStep(scriptStepData.args, resp.state) + runScriptStep(scriptStepDefinition.args, resp.state) ).resolves.not.toThrow() await expect( - runContainerStep(containerStepData.args, resp.state) + runContainerStep(runContainerStepDefinition.args, resp.state) ).resolves.not.toThrow() await expect(cleanupJob(resp, resp.state, null)).resolves.not.toThrow() }) it('should prepare job, then run script step, then run container step with Dockerfile then cleanup', async () => { await expect( - prepareJob(prepareJobData.args, prepareJobOutputFilePath) + prepareJob(prepareJobDefinition.args, prepareJobOutputFilePath) ).resolves.not.toThrow() let rawState = fs.readFileSync(prepareJobOutputFilePath, 'utf-8') let resp = JSON.parse(rawState) await expect( - runScriptStep(scriptStepData.args, resp.state) + runScriptStep(scriptStepDefinition.args, resp.state) ).resolves.not.toThrow() const dockerfilePath = `${tmpOutputDir}/Dockerfile` @@ -104,7 +109,9 @@ ENV TEST=test ENTRYPOINT [ "tail", "-f", "/dev/null" ] ` ) - const containerStepDataCopy = JSON.parse(JSON.stringify(containerStepData)) + const containerStepDataCopy = JSON.parse( + JSON.stringify(runContainerStepDefinition) + ) process.env.GITHUB_WORKSPACE = tmpOutputDir containerStepDataCopy.args.dockerfile = 'Dockerfile' containerStepDataCopy.args.context = '.' diff --git a/packages/docker/tests/prepare-job-test.ts b/packages/docker/tests/prepare-job-test.ts index e94aa08..56f8241 100644 --- a/packages/docker/tests/prepare-job-test.ts +++ b/packages/docker/tests/prepare-job-test.ts @@ -7,20 +7,11 @@ jest.useRealTimers() let prepareJobOutputPath: string let prepareJobData: any -const tmpOutputDir = `${__dirname}/_temp/${uuidv4()}` const prepareJobInputPath = `${__dirname}/../../../examples/prepare-job.json` let testSetup: TestSetup describe('prepare job', () => { - beforeAll(() => { - fs.mkdirSync(tmpOutputDir, { recursive: true }) - }) - - afterAll(() => { - fs.rmSync(tmpOutputDir, { recursive: true }) - }) - beforeEach(async () => { testSetup = new TestSetup() testSetup.initialize() @@ -31,9 +22,12 @@ describe('prepare job', () => { prepareJobData.args.container.userMountVolumes = testSetup.userMountVolumes prepareJobData.args.container.systemMountVolumes = testSetup.systemMountVolumes - prepareJobData.args.container.workingDirectory = testSetup.workingDirectory + prepareJobData.args.container.workingDirectory = + testSetup.containerWorkingDirectory - prepareJobOutputPath = `${tmpOutputDir}/prepare-job-output-${uuidv4()}.json` + prepareJobOutputPath = `${ + testSetup.testDir + }/prepare-job-output-${uuidv4()}.json` fs.writeFileSync(prepareJobOutputPath, '') }) diff --git a/packages/docker/tests/test-setup.ts b/packages/docker/tests/test-setup.ts index 58434b9..efb9c9d 100644 --- a/packages/docker/tests/test-setup.ts +++ b/packages/docker/tests/test-setup.ts @@ -1,11 +1,12 @@ import * as fs from 'fs' -import { v4 as uuidv4 } from 'uuid' -import { env } from 'process' import { Mount } from 'hooklib' +import { env } from 'process' +import { v4 as uuidv4 } from 'uuid' export default class TestSetup { private testdir: string private runnerMockDir: string + private runnerMockSubdirs = { work: '_work', externals: 'externals', @@ -15,36 +16,20 @@ export default class TestSetup { githubHome: '_work/_temp/_github_home', githubWorkflow: '_work/_temp/_github_workflow' } - - private readonly projectName = 'example' + private readonly projectName = 'test' constructor() { this.testdir = `${__dirname}/_temp/${uuidv4()}` this.runnerMockDir = `${this.testdir}/runner/_layout` } - private get allTestDirectories() { - const resp = [this.testdir, this.runnerMockDir] - - for (const [key, value] of Object.entries(this.runnerMockSubdirs)) { - resp.push(`${this.runnerMockDir}/${value}`) - } - - resp.push( - `${this.runnerMockDir}/_work/${this.projectName}/${this.projectName}` - ) - - return resp - } - public initialize(): void { for (const dir of this.allTestDirectories) { fs.mkdirSync(dir, { recursive: true }) } - env['RUNNER_NAME'] = 'test' - env[ - 'RUNNER_TEMP' - ] = `${this.runnerMockDir}/${this.runnerMockSubdirs.workTemp}` + env.RUNNER_NAME = 'test' + env.RUNNER_TEMP = `${this.runnerMockDir}/${this.runnerMockSubdirs.workTemp}` + env.GITHUB_WORKSPACE = this.runnerProjectWorkDir } public teardown(): void { @@ -61,6 +46,24 @@ export default class TestSetup { ] } + public get runnerProjectWorkDir() { + return `${this.runnerMockDir}/_work/${this.projectName}/${this.projectName}` + } + + public get testDir() { + return this.testdir + } + + private get allTestDirectories() { + const resp = [this.testdir, this.runnerMockDir, this.runnerProjectWorkDir] + + for (const [key, value] of Object.entries(this.runnerMockSubdirs)) { + resp.push(`${this.runnerMockDir}/${value}`) + } + + return resp + } + public get systemMountVolumes(): Mount[] { return [ { @@ -106,7 +109,32 @@ export default class TestSetup { ] } - public get workingDirectory(): string { + public get containerWorkingDirectory(): string { return `/__w/${this.projectName}/${this.projectName}` } + + public initializeDockerAction(): string { + const actionPath = `${this.testdir}/_actions/example-handle/example-repo/example-branch/mock-directory` + fs.mkdirSync(actionPath, { recursive: true }) + this.writeDockerfile(actionPath) + this.writeEntrypoint(actionPath) + return actionPath + } + + private writeDockerfile(actionPath: string) { + const content = `FROM alpine:3.10 +COPY entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"]` + fs.writeFileSync(`${actionPath}/Dockerfile`, content) + } + + private writeEntrypoint(actionPath) { + const content = `#!/bin/sh -l +echo "Hello $1" +time=$(date) +echo "::set-output name=time::$time"` + const entryPointPath = `${actionPath}/entrypoint.sh` + fs.writeFileSync(entryPointPath, content) + fs.chmodSync(entryPointPath, 0o755) + } }