mirror of
https://github.com/actions/runner-container-hooks.git
synced 2026-01-21 02:10:51 +08:00
* bump actions * experiment using init container to prepare working environment * rm script before continuing * fix * Update packages/k8s/src/hooks/run-script-step.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * leverage exec stat instead of printf * npm update * document the new constraint --------- Co-authored-by: DenisPalnitsky <DenisPalnitsky@users.noreply.github.com>
285 lines
6.8 KiB
TypeScript
285 lines
6.8 KiB
TypeScript
import * as core from '@actions/core'
|
|
import * as k8s from '@kubernetes/client-node'
|
|
import {
|
|
JobContainerInfo,
|
|
ContextPorts,
|
|
PrepareJobArgs,
|
|
writeToResponseFile,
|
|
ServiceContainerInfo
|
|
} from 'hooklib'
|
|
import {
|
|
containerPorts,
|
|
createJobPod,
|
|
isPodContainerAlpine,
|
|
prunePods,
|
|
waitForPodPhases,
|
|
getPrepareJobTimeoutSeconds,
|
|
execCpToPod,
|
|
execPodStep
|
|
} from '../k8s'
|
|
import {
|
|
CONTAINER_VOLUMES,
|
|
DEFAULT_CONTAINER_ENTRY_POINT,
|
|
DEFAULT_CONTAINER_ENTRY_POINT_ARGS,
|
|
generateContainerName,
|
|
mergeContainerWithOptions,
|
|
readExtensionFromFile,
|
|
PodPhase,
|
|
fixArgs,
|
|
prepareJobScript
|
|
} from '../k8s/utils'
|
|
import {
|
|
CONTAINER_EXTENSION_PREFIX,
|
|
getJobPodName,
|
|
JOB_CONTAINER_NAME
|
|
} from './constants'
|
|
import { dirname } from 'path'
|
|
|
|
export async function prepareJob(
|
|
args: PrepareJobArgs,
|
|
responseFile
|
|
): Promise<void> {
|
|
if (!args.container) {
|
|
throw new Error('Job Container is required.')
|
|
}
|
|
|
|
await prunePods()
|
|
|
|
const extension = readExtensionFromFile()
|
|
|
|
let container: k8s.V1Container | undefined = undefined
|
|
if (args.container?.image) {
|
|
container = createContainerSpec(
|
|
args.container,
|
|
JOB_CONTAINER_NAME,
|
|
true,
|
|
extension
|
|
)
|
|
}
|
|
|
|
let services: k8s.V1Container[] = []
|
|
if (args.services?.length) {
|
|
services = args.services.map(service => {
|
|
return createContainerSpec(
|
|
service,
|
|
generateContainerName(service.image),
|
|
false,
|
|
extension
|
|
)
|
|
})
|
|
}
|
|
|
|
if (!container && !services?.length) {
|
|
throw new Error('No containers exist, skipping hook invocation')
|
|
}
|
|
|
|
let createdPod: k8s.V1Pod | undefined = undefined
|
|
try {
|
|
createdPod = await createJobPod(
|
|
getJobPodName(),
|
|
container,
|
|
services,
|
|
args.container.registry,
|
|
extension
|
|
)
|
|
} catch (err) {
|
|
await prunePods()
|
|
core.debug(`createPod failed: ${JSON.stringify(err)}`)
|
|
const message = (err as any)?.response?.body?.message || err
|
|
throw new Error(`failed to create job pod: ${message}`)
|
|
}
|
|
|
|
if (!createdPod?.metadata?.name) {
|
|
throw new Error('created pod should have metadata.name')
|
|
}
|
|
core.debug(
|
|
`Job pod created, waiting for it to come online ${createdPod?.metadata?.name}`
|
|
)
|
|
|
|
const runnerWorkspace = dirname(process.env.RUNNER_WORKSPACE as string)
|
|
|
|
let prepareScript: { containerPath: string; runnerPath: string } | undefined
|
|
if (args.container?.userMountVolumes?.length) {
|
|
prepareScript = prepareJobScript(args.container.userMountVolumes || [])
|
|
}
|
|
|
|
try {
|
|
await waitForPodPhases(
|
|
createdPod.metadata.name,
|
|
new Set([PodPhase.RUNNING]),
|
|
new Set([PodPhase.PENDING]),
|
|
getPrepareJobTimeoutSeconds()
|
|
)
|
|
} catch (err) {
|
|
await prunePods()
|
|
throw new Error(`pod failed to come online with error: ${err}`)
|
|
}
|
|
|
|
await execCpToPod(createdPod.metadata.name, runnerWorkspace, '/__w')
|
|
|
|
if (prepareScript) {
|
|
await execPodStep(
|
|
['sh', '-e', prepareScript.containerPath],
|
|
createdPod.metadata.name,
|
|
JOB_CONTAINER_NAME
|
|
)
|
|
|
|
const promises: Promise<void>[] = []
|
|
for (const vol of args?.container?.userMountVolumes || []) {
|
|
promises.push(
|
|
execCpToPod(
|
|
createdPod.metadata.name,
|
|
vol.sourceVolumePath,
|
|
vol.targetVolumePath
|
|
)
|
|
)
|
|
}
|
|
await Promise.all(promises)
|
|
}
|
|
|
|
core.debug('Job pod is ready for traffic')
|
|
|
|
let isAlpine = false
|
|
try {
|
|
isAlpine = await isPodContainerAlpine(
|
|
createdPod.metadata.name,
|
|
JOB_CONTAINER_NAME
|
|
)
|
|
} catch (err) {
|
|
core.debug(
|
|
`Failed to determine if the pod is alpine: ${JSON.stringify(err)}`
|
|
)
|
|
const message = (err as any)?.response?.body?.message || err
|
|
throw new Error(`failed to determine if the pod is alpine: ${message}`)
|
|
}
|
|
core.debug(`Setting isAlpine to ${isAlpine}`)
|
|
generateResponseFile(responseFile, args, createdPod, isAlpine)
|
|
}
|
|
|
|
function generateResponseFile(
|
|
responseFile: string,
|
|
args: PrepareJobArgs,
|
|
appPod: k8s.V1Pod,
|
|
isAlpine: boolean
|
|
): void {
|
|
if (!appPod.metadata?.name) {
|
|
throw new Error('app pod must have metadata.name specified')
|
|
}
|
|
const response = {
|
|
state: {
|
|
jobPod: appPod.metadata.name
|
|
},
|
|
context: {},
|
|
isAlpine
|
|
}
|
|
|
|
const mainContainer = appPod.spec?.containers?.find(
|
|
c => c.name === JOB_CONTAINER_NAME
|
|
)
|
|
if (mainContainer) {
|
|
const mainContainerContextPorts: ContextPorts = {}
|
|
if (mainContainer?.ports) {
|
|
for (const port of mainContainer.ports) {
|
|
mainContainerContextPorts[port.containerPort] =
|
|
mainContainerContextPorts.hostPort
|
|
}
|
|
}
|
|
|
|
response.context['container'] = {
|
|
image: mainContainer.image,
|
|
ports: mainContainerContextPorts
|
|
}
|
|
}
|
|
|
|
if (args.services?.length) {
|
|
const serviceContainerNames =
|
|
args.services?.map(s => generateContainerName(s.image)) || []
|
|
|
|
response.context['services'] = appPod?.spec?.containers
|
|
?.filter(c => serviceContainerNames.includes(c.name))
|
|
.map(c => {
|
|
const ctxPorts: ContextPorts = {}
|
|
if (c.ports?.length) {
|
|
for (const port of c.ports) {
|
|
if (port.containerPort && port.hostPort) {
|
|
ctxPorts[port.containerPort.toString()] = port.hostPort.toString()
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
image: c.image,
|
|
ports: ctxPorts
|
|
}
|
|
})
|
|
}
|
|
|
|
writeToResponseFile(responseFile, JSON.stringify(response))
|
|
}
|
|
|
|
export function createContainerSpec(
|
|
container: JobContainerInfo | ServiceContainerInfo,
|
|
name: string,
|
|
jobContainer = false,
|
|
extension?: k8s.V1PodTemplateSpec
|
|
): k8s.V1Container {
|
|
if (!container.entryPoint && jobContainer) {
|
|
container.entryPoint = DEFAULT_CONTAINER_ENTRY_POINT
|
|
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
|
|
}
|
|
|
|
const podContainer = {
|
|
name,
|
|
image: container.image,
|
|
ports: containerPorts(container)
|
|
} as k8s.V1Container
|
|
if (container['workingDirectory']) {
|
|
podContainer.workingDir = container['workingDirectory']
|
|
}
|
|
|
|
if (container.entryPoint) {
|
|
podContainer.command = [container.entryPoint]
|
|
}
|
|
|
|
if (container.entryPointArgs && container.entryPointArgs.length > 0) {
|
|
podContainer.args = fixArgs(container.entryPointArgs)
|
|
}
|
|
|
|
podContainer.env = []
|
|
for (const [key, value] of Object.entries(
|
|
container['environmentVariables'] || {}
|
|
)) {
|
|
if (value && key !== 'HOME') {
|
|
podContainer.env.push({ name: key, value })
|
|
}
|
|
}
|
|
|
|
podContainer.env.push({
|
|
name: 'GITHUB_ACTIONS',
|
|
value: 'true'
|
|
})
|
|
|
|
if (!('CI' in (container['environmentVariables'] || {}))) {
|
|
podContainer.env.push({
|
|
name: 'CI',
|
|
value: 'true'
|
|
})
|
|
}
|
|
|
|
podContainer.volumeMounts = CONTAINER_VOLUMES
|
|
|
|
if (!extension) {
|
|
return podContainer
|
|
}
|
|
|
|
const from = extension.spec?.containers?.find(
|
|
c => c.name === CONTAINER_EXTENSION_PREFIX + name
|
|
)
|
|
|
|
if (from) {
|
|
mergeContainerWithOptions(podContainer, from)
|
|
}
|
|
|
|
return podContainer
|
|
}
|