mirror of
https://github.com/actions/runner-container-hooks.git
synced 2025-12-15 09:16:44 +00:00
Fix working directory and write state for appPod to be used in run-script-step (#8)
* added initial entrypoint script * change workingg directory working with addition to fix prepare-job state output * added prepend path * added run-script-step file generation, removed prepend path from container-step and prepare job * latest changes with testing run script step * fix the mounts real fast * cleanup * fix tests * add kind test * add kind yaml to ignore and run it during ci * fix kind option * remove gitignore * lowercase pwd * checkout first! * ignore test file in build.yaml * fixed wrong working directory and added test to run script step testing for the env * handle env's/escaping better * added single quote escape to env escapes * surounded env value with single quote * added spacing around run-container-step, changed examples to actually echo hello world * refactored tests * make sure to escape properly * set addition mounts for container steps * fixup container action mounts Co-authored-by: Thomas Boop <thboop@github.com> Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
This commit is contained in:
@@ -100,8 +100,13 @@ function generateResponseFile(
|
||||
appPod: k8s.V1Pod,
|
||||
isAlpine
|
||||
): void {
|
||||
if (!appPod.metadata?.name) {
|
||||
throw new Error('app pod must have metadata.name specified')
|
||||
}
|
||||
const response = {
|
||||
state: {},
|
||||
state: {
|
||||
jobPod: appPod.metadata.name
|
||||
},
|
||||
context: {},
|
||||
isAlpine
|
||||
}
|
||||
@@ -163,13 +168,11 @@ function createPodSpec(
|
||||
name: string,
|
||||
jobContainer = false
|
||||
): k8s.V1Container {
|
||||
if (!container.entryPointArgs) {
|
||||
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
|
||||
}
|
||||
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
|
||||
if (!container.entryPoint) {
|
||||
container.entryPoint = DEFAULT_CONTAINER_ENTRY_POINT
|
||||
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
|
||||
}
|
||||
|
||||
const podContainer = {
|
||||
name,
|
||||
image: container.image,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as k8s from '@kubernetes/client-node'
|
||||
import * as core from '@actions/core'
|
||||
import * as k8s from '@kubernetes/client-node'
|
||||
import { RunContainerStepArgs } from 'hooklib'
|
||||
import {
|
||||
createJob,
|
||||
@@ -10,8 +10,14 @@ import {
|
||||
waitForJobToComplete,
|
||||
waitForPodPhases
|
||||
} from '../k8s'
|
||||
import {
|
||||
containerVolumes,
|
||||
DEFAULT_CONTAINER_ENTRY_POINT,
|
||||
DEFAULT_CONTAINER_ENTRY_POINT_ARGS,
|
||||
PodPhase,
|
||||
writeEntryPointScript
|
||||
} from '../k8s/utils'
|
||||
import { JOB_CONTAINER_NAME } from './constants'
|
||||
import { containerVolumes, PodPhase } from '../k8s/utils'
|
||||
|
||||
export async function runContainerStep(
|
||||
stepContainer: RunContainerStepArgs
|
||||
@@ -19,13 +25,16 @@ export async function runContainerStep(
|
||||
if (stepContainer.dockerfile) {
|
||||
throw new Error('Building container actions is not currently supported')
|
||||
}
|
||||
|
||||
let secretName: string | undefined = undefined
|
||||
core.debug('')
|
||||
if (stepContainer.environmentVariables) {
|
||||
secretName = await createSecretForEnvs(stepContainer.environmentVariables)
|
||||
}
|
||||
|
||||
core.debug(`Created secret ${secretName} for container job envs`)
|
||||
const container = createPodSpec(stepContainer, secretName)
|
||||
|
||||
const job = await createJob(container)
|
||||
if (!job.metadata?.name) {
|
||||
throw new Error(
|
||||
@@ -35,6 +44,7 @@ export async function runContainerStep(
|
||||
)
|
||||
}
|
||||
core.debug(`Job created, waiting for pod to start: ${job.metadata?.name}`)
|
||||
|
||||
const podName = await getContainerJobPodName(job.metadata.name)
|
||||
await waitForPodPhases(
|
||||
podName,
|
||||
@@ -42,11 +52,16 @@ export async function runContainerStep(
|
||||
new Set([PodPhase.PENDING, PodPhase.UNKNOWN])
|
||||
)
|
||||
core.debug('Container step is running or complete, pulling logs')
|
||||
|
||||
await getPodLogs(podName, JOB_CONTAINER_NAME)
|
||||
|
||||
core.debug('Waiting for container job to complete')
|
||||
await waitForJobToComplete(job.metadata.name)
|
||||
// pod has failed so pull the status code from the container
|
||||
const status = await getPodStatus(podName)
|
||||
if (status?.phase === 'Succeeded') {
|
||||
return 0
|
||||
}
|
||||
if (!status?.containerStatuses?.length) {
|
||||
core.error(
|
||||
`Can't determine container status from response: ${JSON.stringify(
|
||||
@@ -68,9 +83,18 @@ function createPodSpec(
|
||||
const podContainer = new k8s.V1Container()
|
||||
podContainer.name = JOB_CONTAINER_NAME
|
||||
podContainer.image = container.image
|
||||
if (container.entryPoint) {
|
||||
podContainer.command = [container.entryPoint, ...container.entryPointArgs]
|
||||
}
|
||||
|
||||
const { entryPoint, entryPointArgs } = container
|
||||
container.entryPoint = 'sh'
|
||||
|
||||
const { containerPath } = writeEntryPointScript(
|
||||
container.workingDirectory,
|
||||
entryPoint || DEFAULT_CONTAINER_ENTRY_POINT,
|
||||
entryPoint ? entryPointArgs || [] : DEFAULT_CONTAINER_ENTRY_POINT_ARGS
|
||||
)
|
||||
container.entryPointArgs = ['-e', containerPath]
|
||||
podContainer.command = [container.entryPoint, ...container.entryPointArgs]
|
||||
|
||||
if (secretName) {
|
||||
podContainer.envFrom = [
|
||||
{
|
||||
@@ -81,7 +105,7 @@ function createPodSpec(
|
||||
}
|
||||
]
|
||||
}
|
||||
podContainer.volumeMounts = containerVolumes()
|
||||
podContainer.volumeMounts = containerVolumes(undefined, false, true)
|
||||
|
||||
return podContainer
|
||||
}
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import * as fs from 'fs'
|
||||
import { RunScriptStepArgs } from 'hooklib'
|
||||
import { execPodStep } from '../k8s'
|
||||
import { getJobPodName, JOB_CONTAINER_NAME } from './constants'
|
||||
import { writeEntryPointScript } from '../k8s/utils'
|
||||
import { JOB_CONTAINER_NAME } from './constants'
|
||||
|
||||
export async function runScriptStep(
|
||||
args: RunScriptStepArgs,
|
||||
state,
|
||||
responseFile
|
||||
): Promise<void> {
|
||||
const cb = new CommandsBuilder(
|
||||
args.entryPoint,
|
||||
args.entryPointArgs,
|
||||
args.environmentVariables
|
||||
const { entryPoint, entryPointArgs, environmentVariables } = args
|
||||
const { containerPath, runnerPath } = writeEntryPointScript(
|
||||
args.workingDirectory,
|
||||
entryPoint,
|
||||
entryPointArgs,
|
||||
args.prependPath,
|
||||
environmentVariables
|
||||
)
|
||||
await execPodStep(cb.command, getJobPodName(), JOB_CONTAINER_NAME)
|
||||
}
|
||||
|
||||
class CommandsBuilder {
|
||||
constructor(
|
||||
private entryPoint: string,
|
||||
private entryPointArgs: string[],
|
||||
private environmentVariables: { [key: string]: string }
|
||||
) {}
|
||||
|
||||
get command(): string[] {
|
||||
const envCommands: string[] = []
|
||||
if (
|
||||
this.environmentVariables &&
|
||||
Object.entries(this.environmentVariables).length
|
||||
) {
|
||||
for (const [key, value] of Object.entries(this.environmentVariables)) {
|
||||
envCommands.push(`${key}=${value}`)
|
||||
}
|
||||
}
|
||||
return ['env', ...envCommands, this.entryPoint, ...this.entryPointArgs]
|
||||
args.entryPoint = 'sh'
|
||||
args.entryPointArgs = ['-e', containerPath]
|
||||
try {
|
||||
await execPodStep(
|
||||
[args.entryPoint, ...args.entryPointArgs],
|
||||
state.jobPod,
|
||||
JOB_CONTAINER_NAME
|
||||
)
|
||||
} catch (err) {
|
||||
throw new Error(`failed to run script step: ${JSON.stringify(err)}`)
|
||||
} finally {
|
||||
fs.rmSync(runnerPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,12 +190,8 @@ export async function execPodStep(
|
||||
containerName: string,
|
||||
stdin?: stream.Readable
|
||||
): Promise<void> {
|
||||
// TODO, we need to add the path from `prependPath` to the PATH variable. How can we do that? Maybe another exec before running this one?
|
||||
// Maybe something like, get the current path, if these entries aren't in it, add them, then set the current path to that?
|
||||
|
||||
// TODO: how do we set working directory? There doesn't seem to be an easy way to do it. Should we cd then execute our bash script?
|
||||
const exec = new k8s.Exec(kc)
|
||||
return new Promise(async function (resolve, reject) {
|
||||
await new Promise(async function (resolve, reject) {
|
||||
try {
|
||||
await exec.exec(
|
||||
namespace(),
|
||||
@@ -209,16 +205,19 @@ export async function execPodStep(
|
||||
resp => {
|
||||
// kube.exec returns an error if exit code is not 0, but we can't actually get the exit code
|
||||
if (resp.status === 'Success') {
|
||||
resolve()
|
||||
resolve(resp.code)
|
||||
} else {
|
||||
reject(
|
||||
JSON.stringify({ message: resp?.message, details: resp?.details })
|
||||
JSON.stringify({
|
||||
message: resp?.message,
|
||||
details: resp?.details
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
reject(JSON.stringify(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as k8s from '@kubernetes/client-node'
|
||||
import * as fs from 'fs'
|
||||
import { Mount } from 'hooklib'
|
||||
import * as path from 'path'
|
||||
import { v1 as uuidv4 } from 'uuid'
|
||||
import { POD_VOLUME_NAME } from './index'
|
||||
|
||||
export const DEFAULT_CONTAINER_ENTRY_POINT_ARGS = [`-f`, `/dev/null`]
|
||||
@@ -8,7 +10,8 @@ export const DEFAULT_CONTAINER_ENTRY_POINT = 'tail'
|
||||
|
||||
export function containerVolumes(
|
||||
userMountVolumes: Mount[] = [],
|
||||
jobContainer = true
|
||||
jobContainer = true,
|
||||
containerAction = false
|
||||
): k8s.V1VolumeMount[] {
|
||||
const mounts: k8s.V1VolumeMount[] = [
|
||||
{
|
||||
@@ -17,6 +20,23 @@ export function containerVolumes(
|
||||
}
|
||||
]
|
||||
|
||||
if (containerAction) {
|
||||
const workspace = process.env.GITHUB_WORKSPACE as string
|
||||
mounts.push(
|
||||
{
|
||||
name: POD_VOLUME_NAME,
|
||||
mountPath: '/github/workspace',
|
||||
subPath: workspace.substring(workspace.indexOf('work/') + 1)
|
||||
},
|
||||
{
|
||||
name: POD_VOLUME_NAME,
|
||||
mountPath: '/github/file_commands',
|
||||
subPath: workspace.substring(workspace.indexOf('work/') + 1)
|
||||
}
|
||||
)
|
||||
return mounts
|
||||
}
|
||||
|
||||
if (!jobContainer) {
|
||||
return mounts
|
||||
}
|
||||
@@ -71,6 +91,48 @@ export function containerVolumes(
|
||||
return mounts
|
||||
}
|
||||
|
||||
export function writeEntryPointScript(
|
||||
workingDirectory: string,
|
||||
entryPoint: string,
|
||||
entryPointArgs?: string[],
|
||||
prependPath?: string[],
|
||||
environmentVariables?: { [key: string]: string }
|
||||
): { containerPath: string; runnerPath: string } {
|
||||
let exportPath = ''
|
||||
if (prependPath?.length) {
|
||||
exportPath = `export PATH=${prependPath.join(':')}:$PATH`
|
||||
}
|
||||
let environmentPrefix = ''
|
||||
|
||||
if (environmentVariables && Object.entries(environmentVariables).length) {
|
||||
const envBuffer: string[] = []
|
||||
for (const [key, value] of Object.entries(environmentVariables)) {
|
||||
envBuffer.push(
|
||||
`"${key}=${value
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/=/g, '\\=')}"`
|
||||
)
|
||||
}
|
||||
environmentPrefix = `env ${envBuffer.join(' ')} `
|
||||
}
|
||||
|
||||
const content = `#!/bin/sh -l
|
||||
${exportPath}
|
||||
cd ${workingDirectory} && \
|
||||
exec ${environmentPrefix} ${entryPoint} ${
|
||||
entryPointArgs?.length ? entryPointArgs.join(' ') : ''
|
||||
}
|
||||
`
|
||||
const filename = `${uuidv4()}.sh`
|
||||
const entryPointPath = `${process.env.RUNNER_TEMP}/${filename}`
|
||||
fs.writeFileSync(entryPointPath, content)
|
||||
return {
|
||||
containerPath: `/__w/_temp/${filename}`,
|
||||
runnerPath: entryPointPath
|
||||
}
|
||||
}
|
||||
|
||||
export enum PodPhase {
|
||||
PENDING = 'Pending',
|
||||
RUNNING = 'Running',
|
||||
|
||||
Reference in New Issue
Block a user