mirror of
https://github.com/actions/runner-container-hooks.git
synced 2025-12-14 16:46:43 +00:00
Merge pull request #2 from actions/nikola-jokic/computed-build-directory
Computed action build directory. Refactored tests and added docker build test
This commit is contained in:
@@ -2,10 +2,11 @@ import * as core from '@actions/core'
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import {
|
import {
|
||||||
ContainerInfo,
|
ContainerInfo,
|
||||||
|
Registry,
|
||||||
RunContainerStepArgs,
|
RunContainerStepArgs,
|
||||||
ServiceContainerInfo
|
ServiceContainerInfo
|
||||||
} from 'hooklib/lib'
|
} from 'hooklib/lib'
|
||||||
import 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'
|
||||||
import { runDockerCommand, RunDockerCommandOptions } from '../utils'
|
import { runDockerCommand, RunDockerCommandOptions } from '../utils'
|
||||||
@@ -41,13 +42,9 @@ export async function createContainer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.environmentVariables) {
|
if (args.environmentVariables) {
|
||||||
for (const [key, value] of Object.entries(args.environmentVariables)) {
|
for (const [key] of Object.entries(args.environmentVariables)) {
|
||||||
dockerArgs.push('-e')
|
dockerArgs.push('-e')
|
||||||
if (!value) {
|
dockerArgs.push(key)
|
||||||
dockerArgs.push(`"${key}"`)
|
|
||||||
} else {
|
|
||||||
dockerArgs.push(`"${key}=${value}"`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,17 +141,41 @@ export async function containerBuild(
|
|||||||
args: RunContainerStepArgs,
|
args: RunContainerStepArgs,
|
||||||
tag: string
|
tag: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const context = path.dirname(`${env.GITHUB_WORKSPACE}/${args.dockerfile}`)
|
if (!args.dockerfile) {
|
||||||
|
throw new Error("Container build expects 'args.dockerfile' to be set")
|
||||||
|
}
|
||||||
|
|
||||||
const dockerArgs: string[] = ['build']
|
const dockerArgs: string[] = ['build']
|
||||||
dockerArgs.push('-t', tag)
|
dockerArgs.push('-t', tag)
|
||||||
dockerArgs.push('-f', `${env.GITHUB_WORKSPACE}/${args.dockerfile}`)
|
dockerArgs.push('-f', args.dockerfile)
|
||||||
dockerArgs.push(context)
|
dockerArgs.push(getBuildContext(args.dockerfile))
|
||||||
// TODO: figure out build working directory
|
// TODO: figure out build working directory
|
||||||
await runDockerCommand(dockerArgs, {
|
await runDockerCommand(dockerArgs, {
|
||||||
workingDir: args['buildWorkingDirectory']
|
workingDir: getWorkingDir(args.dockerfile)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBuildContext(dockerfilePath: string): string {
|
||||||
|
return path.dirname(dockerfilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void> {
|
export async function containerLogs(id: string): Promise<void> {
|
||||||
const dockerArgs: string[] = ['logs']
|
const dockerArgs: string[] = ['logs']
|
||||||
dockerArgs.push('--details')
|
dockerArgs.push('--details')
|
||||||
@@ -248,22 +269,22 @@ export async function healthCheck({
|
|||||||
export async function containerPorts(id: string): Promise<string[]> {
|
export async function containerPorts(id: string): Promise<string[]> {
|
||||||
const dockerArgs = ['port', id]
|
const dockerArgs = ['port', id]
|
||||||
const portMappings = (await runDockerCommand(dockerArgs)).trim()
|
const portMappings = (await runDockerCommand(dockerArgs)).trim()
|
||||||
return portMappings.split('\n')
|
return portMappings.split('\n').filter(p => !!p)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registryLogin(args): Promise<string> {
|
export async function registryLogin(registry?: Registry): Promise<string> {
|
||||||
if (!args.registry) {
|
if (!registry) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
const credentials = {
|
const credentials = {
|
||||||
username: args.registry.username,
|
username: registry.username,
|
||||||
password: args.registry.password
|
password: registry.password
|
||||||
}
|
}
|
||||||
|
|
||||||
const configLocation = `${env.RUNNER_TEMP}/.docker_${uuidv4()}`
|
const configLocation = `${env.RUNNER_TEMP}/.docker_${uuidv4()}`
|
||||||
fs.mkdirSync(configLocation)
|
fs.mkdirSync(configLocation)
|
||||||
try {
|
try {
|
||||||
await dockerLogin(configLocation, args.registry.serverUrl, credentials)
|
await dockerLogin(configLocation, registry.serverUrl, credentials)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fs.rmdirSync(configLocation, { recursive: true })
|
fs.rmdirSync(configLocation, { recursive: true })
|
||||||
throw error
|
throw error
|
||||||
@@ -281,7 +302,7 @@ export async function registryLogout(configLocation: string): Promise<void> {
|
|||||||
async function dockerLogin(
|
async function dockerLogin(
|
||||||
configLocation: string,
|
configLocation: string,
|
||||||
registry: string,
|
registry: string,
|
||||||
credentials: { username: string; password: string }
|
credentials: { username?: string; password?: string }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const credentialsArgs =
|
const credentialsArgs =
|
||||||
credentials.username && credentials.password
|
credentials.username && credentials.password
|
||||||
@@ -317,13 +338,9 @@ export async function containerExecStep(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const dockerArgs: string[] = ['exec', '-i']
|
const dockerArgs: string[] = ['exec', '-i']
|
||||||
dockerArgs.push(`--workdir=${args.workingDirectory}`)
|
dockerArgs.push(`--workdir=${args.workingDirectory}`)
|
||||||
for (const [key, value] of Object.entries(args['environmentVariables'])) {
|
for (const [key] of Object.entries(args['environmentVariables'])) {
|
||||||
dockerArgs.push('-e')
|
dockerArgs.push('-e')
|
||||||
if (!value) {
|
dockerArgs.push(key)
|
||||||
dockerArgs.push(`"${key}"`)
|
|
||||||
} else {
|
|
||||||
dockerArgs.push(`"${key}=${value}"`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.prependPath?.length) {
|
if (args.prependPath?.length) {
|
||||||
@@ -341,7 +358,7 @@ export async function containerExecStep(
|
|||||||
export async function containerRun(
|
export async function containerRun(
|
||||||
args: RunContainerStepArgs,
|
args: RunContainerStepArgs,
|
||||||
name: string,
|
name: string,
|
||||||
network: string
|
network?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!args.image) {
|
if (!args.image) {
|
||||||
throw new Error('expected image to be set')
|
throw new Error('expected image to be set')
|
||||||
@@ -351,7 +368,9 @@ export async function containerRun(
|
|||||||
dockerArgs.push('--name', name)
|
dockerArgs.push('--name', name)
|
||||||
dockerArgs.push(`--workdir=${args.workingDirectory}`)
|
dockerArgs.push(`--workdir=${args.workingDirectory}`)
|
||||||
dockerArgs.push(`--label=${getRunnerLabel()}`)
|
dockerArgs.push(`--label=${getRunnerLabel()}`)
|
||||||
dockerArgs.push(`--network=${network}`)
|
if (network) {
|
||||||
|
dockerArgs.push(`--network=${network}`)
|
||||||
|
}
|
||||||
|
|
||||||
if (args.createOptions) {
|
if (args.createOptions) {
|
||||||
dockerArgs.push(...args.createOptions.split(' '))
|
dockerArgs.push(...args.createOptions.split(' '))
|
||||||
|
|||||||
@@ -186,15 +186,15 @@ function transformDockerPortsToContextPorts(
|
|||||||
meta: ContainerMetadata
|
meta: ContainerMetadata
|
||||||
): ContextPorts {
|
): ContextPorts {
|
||||||
// ex: '80/tcp -> 0.0.0.0:80'
|
// ex: '80/tcp -> 0.0.0.0:80'
|
||||||
const re = /^(\d+)\/(\w+)? -> (.*):(\d+)$/
|
const re = /^(\d+)(\/\w+)? -> (.*):(\d+)$/
|
||||||
const contextPorts: ContextPorts = {}
|
const contextPorts: ContextPorts = {}
|
||||||
|
|
||||||
if (meta.ports) {
|
if (meta.ports?.length) {
|
||||||
for (const port of meta.ports) {
|
for (const port of meta.ports) {
|
||||||
const matches = port.match(re)
|
const matches = port.match(re)
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Container ports could not match the regex: "^(\\d+)\\/(\\w+)? -> (.*):(\\d+)$"'
|
'Container ports could not match the regex: "^(\\d+)(\\/\\w+)? -> (.*):(\\d+)$"'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
contextPorts[matches[1]] = matches[matches.length - 1]
|
contextPorts[matches[1]] = matches[matches.length - 1]
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
|
import { RunContainerStepArgs } from 'hooklib/lib'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import {
|
import {
|
||||||
containerBuild,
|
containerBuild,
|
||||||
registryLogin,
|
|
||||||
registryLogout,
|
|
||||||
containerPull,
|
containerPull,
|
||||||
containerRun
|
containerRun,
|
||||||
|
registryLogin,
|
||||||
|
registryLogout
|
||||||
} from '../dockerCommands'
|
} from '../dockerCommands'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import { RunContainerStepArgs } from 'hooklib/lib'
|
|
||||||
import { getRunnerLabel } from '../dockerCommands/constants'
|
import { getRunnerLabel } from '../dockerCommands/constants'
|
||||||
|
|
||||||
export async function runContainerStep(
|
export async function runContainerStep(
|
||||||
@@ -15,23 +14,23 @@ export async function runContainerStep(
|
|||||||
state
|
state
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const tag = generateBuildTag() // for docker build
|
const tag = generateBuildTag() // for docker build
|
||||||
if (!args.image) {
|
if (args.image) {
|
||||||
core.error('expected an image')
|
const configLocation = await registryLogin(args.registry)
|
||||||
} else {
|
try {
|
||||||
if (args.dockerfile) {
|
await containerPull(args.image, configLocation)
|
||||||
await containerBuild(args, tag)
|
} finally {
|
||||||
args.image = tag
|
await registryLogout(configLocation)
|
||||||
} else {
|
|
||||||
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
|
// 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 {
|
function generateBuildTag(): string {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
runContainerStep,
|
runContainerStep,
|
||||||
runScriptStep
|
runScriptStep
|
||||||
} from './hooks'
|
} from './hooks'
|
||||||
|
import { checkEnvironment } from './utils'
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
const input = await getInputFromStdin()
|
const input = await getInputFromStdin()
|
||||||
@@ -23,6 +24,7 @@ async function run(): Promise<void> {
|
|||||||
const state = input['state']
|
const state = input['state']
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
checkEnvironment()
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case Command.PrepareJob:
|
case Command.PrepareJob:
|
||||||
await prepareJob(args as PrepareJobArgs, responseFile)
|
await prepareJob(args as PrepareJobArgs, responseFile)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
/* eslint-disable import/no-commonjs */
|
/* eslint-disable import/no-commonjs */
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
|
import { env } from 'process'
|
||||||
// Import this way otherwise typescript has errors
|
// Import this way otherwise typescript has errors
|
||||||
const exec = require('@actions/exec')
|
const exec = require('@actions/exec')
|
||||||
|
|
||||||
@@ -42,6 +43,12 @@ export function sanitize(val: string): string {
|
|||||||
return newNameBuilder.join('')
|
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
|
// isAlpha accepts single character and checks if
|
||||||
// that character is [a-zA-Z]
|
// that character is [a-zA-Z]
|
||||||
function isAlpha(val: string): boolean {
|
function isAlpha(val: string): boolean {
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ describe('cleanup job', () => {
|
|||||||
const prepareJobOutput = testSetup.createOutputFile(
|
const prepareJobOutput = testSetup.createOutputFile(
|
||||||
'prepare-job-output.json'
|
'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, prepareJobOutput)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
33
packages/docker/tests/container-build-test.ts
Normal file
33
packages/docker/tests/container-build-test.ts
Normal file
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -39,6 +39,10 @@ describe('e2e', () => {
|
|||||||
testSetup.initialize()
|
testSetup.initialize()
|
||||||
definitions.prepareJob.args.container.systemMountVolumes =
|
definitions.prepareJob.args.container.systemMountVolumes =
|
||||||
testSetup.systemMountVolumes
|
testSetup.systemMountVolumes
|
||||||
|
definitions.prepareJob.args.container.registry = null
|
||||||
|
definitions.prepareJob.args.services.forEach(s => {
|
||||||
|
s.registry = null
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ describe('prepare job', () => {
|
|||||||
testSetup.systemMountVolumes
|
testSetup.systemMountVolumes
|
||||||
prepareJobDefinition.args.container.workingDirectory =
|
prepareJobDefinition.args.container.workingDirectory =
|
||||||
testSetup.workingDirectory
|
testSetup.workingDirectory
|
||||||
|
prepareJobDefinition.args.container.registry = null
|
||||||
|
prepareJobDefinition.args.services.forEach(s => {
|
||||||
|
s.registry = null
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ describe('run-script-step', () => {
|
|||||||
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'))
|
||||||
|
|||||||
@@ -115,4 +115,29 @@ export default class TestSetup {
|
|||||||
public get containerWorkingDirectory(): string {
|
public get containerWorkingDirectory(): string {
|
||||||
return `/__w/${this.projectName}/${this.projectName}`
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user