mirror of
https://github.com/actions/runner-container-hooks.git
synced 2025-12-17 10:16:44 +00:00
Initial Commit
This commit is contained in:
413
packages/docker/src/dockerCommands/container.ts
Normal file
413
packages/docker/src/dockerCommands/container.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fs from 'fs'
|
||||
import {
|
||||
ContainerInfo,
|
||||
JobContainerInfo,
|
||||
RunContainerStepArgs,
|
||||
ServiceContainerInfo,
|
||||
StepContainerInfo
|
||||
} from 'hooklib/lib'
|
||||
import path from 'path'
|
||||
import { env } from 'process'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { runDockerCommand, RunDockerCommandOptions } from '../utils'
|
||||
import { getRunnerLabel } from './constants'
|
||||
|
||||
export async function createContainer(
|
||||
args: ContainerInfo,
|
||||
name: string,
|
||||
network: string
|
||||
): Promise<ContainerMetadata> {
|
||||
if (!args.image) {
|
||||
throw new Error('Image was expected')
|
||||
}
|
||||
|
||||
const dockerArgs: string[] = ['create']
|
||||
dockerArgs.push(`--label=${getRunnerLabel()}`)
|
||||
dockerArgs.push(`--network=${network}`)
|
||||
if ((args as ServiceContainerInfo)?.contextName) {
|
||||
dockerArgs.push(
|
||||
`--network-alias=${(args as ServiceContainerInfo)?.contextName}`
|
||||
)
|
||||
}
|
||||
|
||||
dockerArgs.push('--name', name)
|
||||
|
||||
if (args?.portMappings?.length) {
|
||||
for (const portMapping of args.portMappings) {
|
||||
dockerArgs.push('-p', portMapping)
|
||||
}
|
||||
}
|
||||
if (args.createOptions) {
|
||||
dockerArgs.push(...args.createOptions.split(' '))
|
||||
}
|
||||
|
||||
if (args.environmentVariables) {
|
||||
for (const [key, value] of Object.entries(args.environmentVariables)) {
|
||||
dockerArgs.push('-e')
|
||||
if (!value) {
|
||||
dockerArgs.push(`"${key}"`)
|
||||
} else {
|
||||
dockerArgs.push(`"${key}=${value}"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mountVolumes = [
|
||||
...(args.userMountVolumes || []),
|
||||
...((args as JobContainerInfo | StepContainerInfo).systemMountVolumes || [])
|
||||
]
|
||||
for (const mountVolume of mountVolumes) {
|
||||
dockerArgs.push(
|
||||
`-v=${mountVolume.sourceVolumePath}:${mountVolume.targetVolumePath}`
|
||||
)
|
||||
}
|
||||
if (args.entryPoint) {
|
||||
dockerArgs.push(`--entrypoint`)
|
||||
dockerArgs.push(args.entryPoint)
|
||||
}
|
||||
|
||||
dockerArgs.push(args.image)
|
||||
if (args.entryPointArgs) {
|
||||
for (const entryPointArg of args.entryPointArgs) {
|
||||
dockerArgs.push(entryPointArg)
|
||||
}
|
||||
}
|
||||
|
||||
const id = (await runDockerCommand(dockerArgs)).trim()
|
||||
if (!id) {
|
||||
throw new Error('Could not read id from docker command')
|
||||
}
|
||||
const response: ContainerMetadata = { id, image: args.image }
|
||||
if (network) {
|
||||
response.network = network
|
||||
}
|
||||
response.ports = []
|
||||
|
||||
if ((args as ServiceContainerInfo).contextName) {
|
||||
response['contextName'] = (args as ServiceContainerInfo).contextName
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export async function containerPull(
|
||||
image: string,
|
||||
configLocation: string
|
||||
): Promise<void> {
|
||||
const dockerArgs: string[] = ['pull']
|
||||
if (configLocation) {
|
||||
dockerArgs.push('--config')
|
||||
dockerArgs.push(configLocation)
|
||||
}
|
||||
dockerArgs.push(image)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
await runDockerCommand(dockerArgs)
|
||||
return
|
||||
} catch {
|
||||
core.info(`docker pull failed on attempt: ${i + 1}`)
|
||||
}
|
||||
}
|
||||
throw new Error('Exiting docker pull after 3 failed attempts')
|
||||
}
|
||||
|
||||
export async function containerStart(id: string): Promise<void> {
|
||||
const dockerArgs: string[] = ['start']
|
||||
dockerArgs.push(`${id}`)
|
||||
await runDockerCommand(dockerArgs)
|
||||
}
|
||||
|
||||
export async function containerStop(id: string | string[]): Promise<void> {
|
||||
const dockerArgs: string[] = ['stop']
|
||||
if (Array.isArray(id)) {
|
||||
for (const v of id) {
|
||||
dockerArgs.push(v)
|
||||
}
|
||||
} else {
|
||||
dockerArgs.push(id)
|
||||
}
|
||||
await runDockerCommand(dockerArgs)
|
||||
}
|
||||
|
||||
export async function containerRemove(id: string | string[]): Promise<void> {
|
||||
const dockerArgs: string[] = ['rm']
|
||||
dockerArgs.push('--force')
|
||||
if (Array.isArray(id)) {
|
||||
for (const v of id) {
|
||||
dockerArgs.push(v)
|
||||
}
|
||||
} else {
|
||||
dockerArgs.push(id)
|
||||
}
|
||||
await runDockerCommand(dockerArgs)
|
||||
}
|
||||
|
||||
export async function containerBuild(
|
||||
args: RunContainerStepArgs,
|
||||
tag: string
|
||||
): Promise<void> {
|
||||
const context = path.dirname(`${env.GITHUB_WORKSPACE}/${args.dockerfile}`)
|
||||
const dockerArgs: string[] = ['build']
|
||||
dockerArgs.push('-t', tag)
|
||||
dockerArgs.push('-f', `${env.GITHUB_WORKSPACE}/${args.dockerfile}`)
|
||||
dockerArgs.push(context)
|
||||
// TODO: figure out build working directory
|
||||
await runDockerCommand(dockerArgs, {
|
||||
workingDir: args['buildWorkingDirectory']
|
||||
})
|
||||
}
|
||||
|
||||
export async function containerLogs(id: string): Promise<void> {
|
||||
const dockerArgs: string[] = ['logs']
|
||||
dockerArgs.push('--details')
|
||||
dockerArgs.push(id)
|
||||
await runDockerCommand(dockerArgs)
|
||||
}
|
||||
|
||||
export async function containerNetworkRemove(network: string): Promise<void> {
|
||||
const dockerArgs: string[] = ['network']
|
||||
dockerArgs.push('rm')
|
||||
dockerArgs.push(network)
|
||||
await runDockerCommand(dockerArgs)
|
||||
}
|
||||
|
||||
export async function containerPrune(): Promise<void> {
|
||||
const dockerPSArgs: string[] = [
|
||||
'ps',
|
||||
'--all',
|
||||
'--quiet',
|
||||
'--no-trunc',
|
||||
'--filter',
|
||||
`label=${getRunnerLabel()}`
|
||||
]
|
||||
|
||||
const res = (await runDockerCommand(dockerPSArgs)).trim()
|
||||
if (res) {
|
||||
await containerRemove(res.split('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
async function containerHealthStatus(id: string): Promise<ContainerHealth> {
|
||||
const dockerArgs = [
|
||||
'inspect',
|
||||
'--format="{{if .Config.Healthcheck}}{{print .State.Health.Status}}{{end}}"',
|
||||
id
|
||||
]
|
||||
const result = (await runDockerCommand(dockerArgs)).trim().replace(/"/g, '')
|
||||
if (
|
||||
result === ContainerHealth.Healthy ||
|
||||
result === ContainerHealth.Starting ||
|
||||
result === ContainerHealth.Unhealthy
|
||||
) {
|
||||
return result
|
||||
}
|
||||
|
||||
return ContainerHealth.None
|
||||
}
|
||||
|
||||
export async function healthCheck({
|
||||
id,
|
||||
image
|
||||
}: ContainerMetadata): Promise<void> {
|
||||
let health = await containerHealthStatus(id)
|
||||
if (health === ContainerHealth.None) {
|
||||
core.info(
|
||||
`Healthcheck is not set for container ${image}, considered as ${ContainerHealth.Healthy}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let tries = 1
|
||||
while (health === ContainerHealth.Starting && tries < 13) {
|
||||
const backOffSeconds = Math.pow(2, tries)
|
||||
core.info(
|
||||
`Container '${image}' is '${health}', retry in ${backOffSeconds} seconds`
|
||||
)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * backOffSeconds))
|
||||
tries++
|
||||
health = await containerHealthStatus(id)
|
||||
}
|
||||
|
||||
if (health !== ContainerHealth.Healthy) {
|
||||
throw new String(
|
||||
`Container '${image}' is unhealthy with status '${health}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function containerPorts(id: string): Promise<string[]> {
|
||||
const dockerArgs = ['port', id]
|
||||
const portMappings = (await runDockerCommand(dockerArgs)).trim()
|
||||
return portMappings.split('\n')
|
||||
}
|
||||
|
||||
export async function registryLogin(args): Promise<string> {
|
||||
if (!args.registry) {
|
||||
return ''
|
||||
}
|
||||
const credentials = {
|
||||
username: args.registry.username,
|
||||
password: args.registry.password
|
||||
}
|
||||
|
||||
const configLocation = `${env.RUNNER_TEMP}/.docker_${uuidv4()}`
|
||||
fs.mkdirSync(configLocation)
|
||||
try {
|
||||
await dockerLogin(configLocation, args.registry.serverUrl, credentials)
|
||||
} catch (error) {
|
||||
fs.rmdirSync(configLocation, { recursive: true })
|
||||
throw error
|
||||
}
|
||||
return configLocation
|
||||
}
|
||||
|
||||
export async function registryLogout(configLocation: string): Promise<void> {
|
||||
if (configLocation) {
|
||||
await dockerLogout(configLocation)
|
||||
fs.rmdirSync(configLocation, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function dockerLogin(
|
||||
configLocation: string,
|
||||
registry: string,
|
||||
credentials: { username: string; password: string }
|
||||
): Promise<void> {
|
||||
const credentialsArgs =
|
||||
credentials.username && credentials.password
|
||||
? ['-u', credentials.username, '--password-stdin']
|
||||
: []
|
||||
|
||||
const dockerArgs = [
|
||||
'--config',
|
||||
configLocation,
|
||||
'login',
|
||||
...credentialsArgs,
|
||||
registry
|
||||
]
|
||||
|
||||
const options: RunDockerCommandOptions =
|
||||
credentials.username && credentials.password
|
||||
? {
|
||||
input: Buffer.from(credentials.password, 'utf-8')
|
||||
}
|
||||
: {}
|
||||
|
||||
await runDockerCommand(dockerArgs, options)
|
||||
}
|
||||
|
||||
async function dockerLogout(configLocation: string): Promise<void> {
|
||||
const dockerArgs = ['--config', configLocation, 'logout']
|
||||
await runDockerCommand(dockerArgs)
|
||||
}
|
||||
|
||||
export async function containerExecStep(
|
||||
args,
|
||||
containerId: string
|
||||
): Promise<void> {
|
||||
const dockerArgs: string[] = ['exec', '-i']
|
||||
dockerArgs.push(`--workdir=${args.workingDirectory}`)
|
||||
for (const [key, value] of Object.entries(args['environmentVariables'])) {
|
||||
dockerArgs.push('-e')
|
||||
if (!value) {
|
||||
dockerArgs.push(`"${key}"`)
|
||||
} else {
|
||||
dockerArgs.push(`"${key}=${value}"`)
|
||||
}
|
||||
}
|
||||
|
||||
// Todo figure out prepend path and update it here
|
||||
// (we need to pass path in as -e Path={fullpath}) where {fullpath is the prepend path added to the current containers path}
|
||||
|
||||
dockerArgs.push(containerId)
|
||||
dockerArgs.push(args.entryPoint)
|
||||
for (const entryPointArg of args.entryPointArgs) {
|
||||
dockerArgs.push(entryPointArg)
|
||||
}
|
||||
await runDockerCommand(dockerArgs)
|
||||
}
|
||||
|
||||
export async function containerRun(
|
||||
args: RunContainerStepArgs,
|
||||
name: string,
|
||||
network: string
|
||||
): Promise<void> {
|
||||
if (!args.image) {
|
||||
throw new Error('expected image to be set')
|
||||
}
|
||||
const dockerArgs: string[] = ['run', '--rm']
|
||||
|
||||
dockerArgs.push('--name', name)
|
||||
dockerArgs.push(`--workdir=${args.workingDirectory}`)
|
||||
dockerArgs.push(`--label=${getRunnerLabel()}`)
|
||||
dockerArgs.push(`--network=${network}`)
|
||||
|
||||
if (args.createOptions) {
|
||||
dockerArgs.push(...args.createOptions.split(' '))
|
||||
}
|
||||
if (args.environmentVariables) {
|
||||
for (const [key, value] of Object.entries(args.environmentVariables)) {
|
||||
// Pass in this way to avoid printing secrets
|
||||
env[key] = value ?? undefined
|
||||
dockerArgs.push('-e')
|
||||
dockerArgs.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
const mountVolumes = [
|
||||
...(args.userMountVolumes || []),
|
||||
...(args.systemMountVolumes || [])
|
||||
]
|
||||
for (const mountVolume of mountVolumes) {
|
||||
dockerArgs.push(`-v`)
|
||||
dockerArgs.push(
|
||||
`${mountVolume.sourceVolumePath}:${mountVolume.targetVolumePath}${
|
||||
mountVolume.readOnly ? ':ro' : ''
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
if (args['entryPoint']) {
|
||||
dockerArgs.push(`--entrypoint`)
|
||||
dockerArgs.push(args['entryPoint'])
|
||||
}
|
||||
dockerArgs.push(args.image)
|
||||
if (args.entryPointArgs) {
|
||||
for (const entryPointArg of args.entryPointArgs) {
|
||||
dockerArgs.push(entryPointArg)
|
||||
}
|
||||
}
|
||||
|
||||
await runDockerCommand(dockerArgs)
|
||||
}
|
||||
|
||||
export async function isContainerAlpine(containerId: string): Promise<boolean> {
|
||||
const dockerArgs: string[] = [
|
||||
'exec',
|
||||
containerId,
|
||||
'sh',
|
||||
'-c',
|
||||
"[ $(cat /etc/*release* | grep -i -e '^ID=*alpine*' -c) != 0 ] || exit 1"
|
||||
]
|
||||
try {
|
||||
await runDockerCommand(dockerArgs)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
enum ContainerHealth {
|
||||
Starting = 'starting',
|
||||
Healthy = 'healthy',
|
||||
Unhealthy = 'unhealthy',
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
export interface ContainerMetadata {
|
||||
id: string
|
||||
image: string
|
||||
network?: string
|
||||
ports?: string[]
|
||||
contextName?: string
|
||||
}
|
||||
Reference in New Issue
Block a user