mirror of
https://github.com/actions/runner-container-hooks.git
synced 2025-12-16 09:46:43 +00:00
Initial Commit
This commit is contained in:
205
packages/docker/src/hooks/prepare-job.ts
Normal file
205
packages/docker/src/hooks/prepare-job.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import * as core from '@actions/core'
|
||||
import { ContextPorts, PrepareJobArgs, writeToResponseFile } from 'hooklib/lib'
|
||||
import { exit } from 'process'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
ContainerMetadata,
|
||||
containerPorts,
|
||||
containerPrune,
|
||||
containerPull,
|
||||
containerStart,
|
||||
createContainer,
|
||||
healthCheck,
|
||||
isContainerAlpine,
|
||||
registryLogin,
|
||||
registryLogout
|
||||
} from '../dockerCommands/container'
|
||||
import { networkCreate, networkPrune } from '../dockerCommands/network'
|
||||
import { sanitize } from '../utils'
|
||||
|
||||
export async function prepareJob(
|
||||
args: PrepareJobArgs,
|
||||
responseFile
|
||||
): Promise<void> {
|
||||
await containerPrune()
|
||||
await networkPrune()
|
||||
|
||||
const container = args.container
|
||||
const services = args.services
|
||||
|
||||
if (!container?.image && !services?.length) {
|
||||
core.info('No containers exist, skipping hook invocation')
|
||||
exit(0)
|
||||
}
|
||||
const networkName = generateNetworkName()
|
||||
// Create network
|
||||
await networkCreate(networkName)
|
||||
|
||||
// Create Job Container
|
||||
let containerMetadata: ContainerMetadata | undefined = undefined
|
||||
if (!container?.image) {
|
||||
core.info('No job container provided, skipping')
|
||||
} else {
|
||||
setupContainer(container)
|
||||
|
||||
const configLocation = await registryLogin(container.registry)
|
||||
try {
|
||||
await containerPull(container.image, configLocation)
|
||||
} finally {
|
||||
await registryLogout(configLocation)
|
||||
}
|
||||
containerMetadata = await createContainer(
|
||||
container,
|
||||
generateContainerName(container.image),
|
||||
networkName
|
||||
)
|
||||
if (!containerMetadata?.id) {
|
||||
throw new Error('Failed to create container')
|
||||
}
|
||||
await containerStart(containerMetadata?.id)
|
||||
}
|
||||
|
||||
// Create Service Containers
|
||||
const servicesMetadata: ContainerMetadata[] = []
|
||||
if (!services?.length) {
|
||||
core.info('No service containers provided, skipping')
|
||||
} else {
|
||||
for (const service of services) {
|
||||
const configLocation = await registryLogin(service.registry)
|
||||
try {
|
||||
await containerPull(service.image, configLocation)
|
||||
} finally {
|
||||
await registryLogout(configLocation)
|
||||
}
|
||||
|
||||
setupContainer(service)
|
||||
const response = await createContainer(
|
||||
service,
|
||||
generateContainerName(service.image),
|
||||
networkName
|
||||
)
|
||||
servicesMetadata.push(response)
|
||||
await containerStart(response.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(container && !containerMetadata?.id) ||
|
||||
(services?.length && servicesMetadata.some(s => !s.id))
|
||||
) {
|
||||
throw new Error(
|
||||
`Not all containers are started correctly ${
|
||||
containerMetadata?.id
|
||||
}, ${servicesMetadata.map(e => e.id).join(',')}`
|
||||
)
|
||||
}
|
||||
|
||||
const isAlpine = await isContainerAlpine(containerMetadata!.id)
|
||||
|
||||
if (containerMetadata?.id) {
|
||||
containerMetadata.ports = await containerPorts(containerMetadata.id)
|
||||
}
|
||||
if (servicesMetadata?.length) {
|
||||
for (const serviceMetadata of servicesMetadata) {
|
||||
serviceMetadata.ports = await containerPorts(serviceMetadata.id)
|
||||
}
|
||||
}
|
||||
|
||||
const healthChecks: Promise<void>[] = [healthCheck(containerMetadata!)]
|
||||
for (const service of servicesMetadata) {
|
||||
healthChecks.push(healthCheck(service))
|
||||
}
|
||||
try {
|
||||
await Promise.all(healthChecks)
|
||||
core.info('All services are healthy')
|
||||
} catch (error) {
|
||||
core.error(`Failed to initialize containers, ${error}`)
|
||||
throw new Error(`Failed to initialize containers, ${error}`)
|
||||
}
|
||||
|
||||
generateResponseFile(
|
||||
responseFile,
|
||||
networkName,
|
||||
containerMetadata,
|
||||
servicesMetadata,
|
||||
isAlpine
|
||||
)
|
||||
}
|
||||
|
||||
function generateResponseFile(
|
||||
responseFile: string,
|
||||
networkName: string,
|
||||
containerMetadata?: ContainerMetadata,
|
||||
servicesMetadata?: ContainerMetadata[],
|
||||
isAlpine = false
|
||||
): void {
|
||||
// todo figure out if we are alpine
|
||||
const response = {
|
||||
state: { network: networkName },
|
||||
context: {},
|
||||
isAlpine
|
||||
}
|
||||
if (containerMetadata) {
|
||||
response.state['container'] = containerMetadata.id
|
||||
const contextMeta = JSON.parse(JSON.stringify(containerMetadata))
|
||||
if (containerMetadata.ports) {
|
||||
contextMeta.ports = transformDockerPortsToContextPorts(containerMetadata)
|
||||
}
|
||||
response.context['container'] = contextMeta
|
||||
|
||||
if (containerMetadata.ports) {
|
||||
response.context['container'].ports =
|
||||
transformDockerPortsToContextPorts(containerMetadata)
|
||||
}
|
||||
}
|
||||
if (servicesMetadata && servicesMetadata.length > 0) {
|
||||
response.state['services'] = []
|
||||
response.context['services'] = []
|
||||
for (const meta of servicesMetadata) {
|
||||
response.state['services'].push(meta.id)
|
||||
const contextMeta = JSON.parse(JSON.stringify(meta))
|
||||
if (contextMeta.ports) {
|
||||
contextMeta.ports = transformDockerPortsToContextPorts(contextMeta)
|
||||
}
|
||||
response.context['services'].push(contextMeta)
|
||||
}
|
||||
}
|
||||
writeToResponseFile(responseFile, JSON.stringify(response))
|
||||
}
|
||||
|
||||
function setupContainer(container): void {
|
||||
container.entryPointArgs = [`-f`, `/dev/null`]
|
||||
container.entryPoint = 'tail'
|
||||
}
|
||||
|
||||
function generateNetworkName(): string {
|
||||
return `github_network_${uuidv4()}`
|
||||
}
|
||||
|
||||
function generateContainerName(container): string {
|
||||
const randomAlias = uuidv4().replace(/-/g, '')
|
||||
const randomSuffix = uuidv4().substring(0, 6)
|
||||
return `${randomAlias}_${sanitize(container.image)}_${randomSuffix}`
|
||||
}
|
||||
|
||||
function transformDockerPortsToContextPorts(
|
||||
meta: ContainerMetadata
|
||||
): ContextPorts {
|
||||
// ex: '80/tcp -> 0.0.0.0:80'
|
||||
const re = /^(\d+)\/(\w+)? -> (.*):(\d+)$/
|
||||
const contextPorts: ContextPorts = {}
|
||||
|
||||
if (meta.ports) {
|
||||
for (const port of meta.ports) {
|
||||
const matches = port.match(re)
|
||||
if (!matches) {
|
||||
throw new Error(
|
||||
'Container ports could not match the regex: "^(\\d+)\\/(\\w+)? -> (.*):(\\d+)$"'
|
||||
)
|
||||
}
|
||||
contextPorts[matches[1]] = matches[matches.length - 1]
|
||||
}
|
||||
}
|
||||
|
||||
return contextPorts
|
||||
}
|
||||
Reference in New Issue
Block a user