Merge pull request #7 from actions/thboop/refactor3

K8s hook refactor
This commit is contained in:
Thomas Boop
2022-06-09 09:33:53 -04:00
committed by GitHub
7 changed files with 56 additions and 37 deletions

View File

@@ -74,14 +74,6 @@ export enum Protocol {
UDP = 'udp' UDP = 'udp'
} }
export enum PodPhase {
PENDING = 'Pending',
RUNNING = 'Running',
SUCCEEDED = 'Succeeded',
FAILED = 'Failed',
UNKNOWN = 'Unknown'
}
export interface PrepareJobResponse { export interface PrepareJobResponse {
state?: object state?: object
context?: ContainerContext context?: ContainerContext

View File

@@ -27,3 +27,10 @@ Some things are expected to be set when using these hooks
- Some actions runner env's are expected to be set. These are set automatically by the runner. - Some actions runner env's are expected to be set. These are set automatically by the runner.
- `RUNNER_WORKSPACE` is expected to be set to the workspace of the runner - `RUNNER_WORKSPACE` is expected to be set to the workspace of the runner
- `GITHUB_WORKSPACE` is expected to be set to the workspace of the job - `GITHUB_WORKSPACE` is expected to be set to the workspace of the job
## Limitations
- Container actions
- Building container actions from a dockerfile is not supported at this time
- Container actions will not have access to the services network or job container network
- Docker [create options](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontaineroptions) are not supported

View File

@@ -2,11 +2,12 @@ import * as core from '@actions/core'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as k8s from '@kubernetes/client-node' import * as k8s from '@kubernetes/client-node'
import { import {
ContextPorts,
PodPhase, PodPhase,
prepareJobArgs, containerVolumes,
writeToResponseFile DEFAULT_CONTAINER_ENTRY_POINT,
} from 'hooklib' DEFAULT_CONTAINER_ENTRY_POINT_ARGS
} from '../k8s/utils'
import { ContextPorts, prepareJobArgs, writeToResponseFile } from 'hooklib'
import path from 'path' import path from 'path'
import { import {
containerPorts, containerPorts,
@@ -18,11 +19,6 @@ import {
requiredPermissions, requiredPermissions,
waitForPodPhases waitForPodPhases
} from '../k8s' } from '../k8s'
import {
containerVolumes,
DEFAULT_CONTAINER_ENTRY_POINT,
DEFAULT_CONTAINER_ENTRY_POINT_ARGS
} from '../k8s/utils'
import { JOB_CONTAINER_NAME } from './constants' import { JOB_CONTAINER_NAME } from './constants'
export async function prepareJob( export async function prepareJob(
@@ -40,14 +36,14 @@ export async function prepareJob(
await copyExternalsToRoot() await copyExternalsToRoot()
let container: k8s.V1Container | undefined = undefined let container: k8s.V1Container | undefined = undefined
if (args.container?.image) { if (args.container?.image) {
core.info(`Using image '${args.container.image}' for job image`) core.debug(`Using image '${args.container.image}' for job image`)
container = createPodSpec(args.container, JOB_CONTAINER_NAME, true) container = createPodSpec(args.container, JOB_CONTAINER_NAME, true)
} }
let services: k8s.V1Container[] = [] let services: k8s.V1Container[] = []
if (args.services?.length) { if (args.services?.length) {
services = args.services.map(service => { services = args.services.map(service => {
core.info(`Adding service '${service.image}' to pod definition`) core.debug(`Adding service '${service.image}' to pod definition`)
return createPodSpec(service, service.image.split(':')[0]) return createPodSpec(service, service.image.split(':')[0])
}) })
} }
@@ -65,6 +61,9 @@ export async function prepareJob(
if (!createdPod?.metadata?.name) { if (!createdPod?.metadata?.name) {
throw new Error('created pod should have 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}`
)
try { try {
await waitForPodPhases( await waitForPodPhases(
@@ -77,7 +76,7 @@ export async function prepareJob(
throw new Error(`Pod failed to come online with error: ${err}`) throw new Error(`Pod failed to come online with error: ${err}`)
} }
core.info('Pod is ready for traffic') core.debug('Job pod is ready for traffic')
let isAlpine = false let isAlpine = false
try { try {
@@ -88,7 +87,7 @@ export async function prepareJob(
} catch (err) { } catch (err) {
throw new Error(`Failed to determine if the pod is alpine: ${err}`) throw new Error(`Failed to determine if the pod is alpine: ${err}`)
} }
core.debug(`Setting isAlpine to ${isAlpine}`)
generateResponseFile(responseFile, createdPod, isAlpine) generateResponseFile(responseFile, createdPod, isAlpine)
} }
@@ -160,7 +159,6 @@ function createPodSpec(
name: string, name: string,
jobContainer = false jobContainer = false
): k8s.V1Container { ): k8s.V1Container {
core.info(JSON.stringify(container))
if (!container.entryPointArgs) { if (!container.entryPointArgs) {
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
} }

View File

@@ -1,6 +1,6 @@
import * as k8s from '@kubernetes/client-node' import * as k8s from '@kubernetes/client-node'
import * as core from '@actions/core' import * as core from '@actions/core'
import { PodPhase } from 'hooklib' import { RunContainerStepArgs } from 'hooklib'
import { import {
createJob, createJob,
createSecretForEnvs, createSecretForEnvs,
@@ -11,18 +11,20 @@ import {
waitForPodPhases waitForPodPhases
} from '../k8s' } from '../k8s'
import { JOB_CONTAINER_NAME } from './constants' import { JOB_CONTAINER_NAME } from './constants'
import { containerVolumes } from '../k8s/utils' import { containerVolumes, PodPhase } from '../k8s/utils'
export async function runContainerStep(stepContainer): Promise<number> { export async function runContainerStep(
stepContainer: RunContainerStepArgs
): Promise<number> {
if (stepContainer.dockerfile) { if (stepContainer.dockerfile) {
throw new Error('Building container actions is not currently supported') throw new Error('Building container actions is not currently supported')
} }
let secretName: string | undefined = undefined let secretName: string | undefined = undefined
if (stepContainer['environmentVariables']) { core.debug('')
secretName = await createSecretForEnvs( if (stepContainer.environmentVariables) {
stepContainer['environmentVariables'] secretName = await createSecretForEnvs(stepContainer.environmentVariables)
)
} }
core.debug(`Created secret ${secretName} for container job envs`)
const container = createPodSpec(stepContainer, secretName) const container = createPodSpec(stepContainer, secretName)
const job = await createJob(container) const job = await createJob(container)
if (!job.metadata?.name) { if (!job.metadata?.name) {
@@ -32,27 +34,37 @@ export async function runContainerStep(stepContainer): Promise<number> {
)} to have correctly set the metadata.name` )} to have correctly set the metadata.name`
) )
} }
core.debug(`Job created, waiting for pod to start: ${job.metadata?.name}`)
const podName = await getContainerJobPodName(job.metadata.name) const podName = await getContainerJobPodName(job.metadata.name)
await waitForPodPhases( await waitForPodPhases(
podName, podName,
new Set([PodPhase.COMPLETED, PodPhase.RUNNING, PodPhase.SUCCEEDED]), new Set([PodPhase.COMPLETED, PodPhase.RUNNING, PodPhase.SUCCEEDED]),
new Set([PodPhase.PENDING, PodPhase.UNKNOWN]) new Set([PodPhase.PENDING, PodPhase.UNKNOWN])
) )
core.debug('Container step is running or complete, pulling logs')
await getPodLogs(podName, JOB_CONTAINER_NAME) await getPodLogs(podName, JOB_CONTAINER_NAME)
core.debug('Waiting for container job to complete')
await waitForJobToComplete(job.metadata.name) await waitForJobToComplete(job.metadata.name)
// pod has failed so pull the status code from the container // pod has failed so pull the status code from the container
const status = await getPodStatus(podName) const status = await getPodStatus(podName)
if (!status?.containerStatuses?.length) { if (!status?.containerStatuses?.length) {
core.warning(`Can't determine container status`) core.error(
return 0 `Can't determine container status from response: ${JSON.stringify(
status
)}`
)
return 1
} }
const exitCode = const exitCode =
status.containerStatuses[status.containerStatuses.length - 1].state status.containerStatuses[status.containerStatuses.length - 1].state
?.terminated?.exitCode ?.terminated?.exitCode
return Number(exitCode) || 0 return Number(exitCode) || 1
} }
function createPodSpec(container, secretName?: string): k8s.V1Container { function createPodSpec(
container: RunContainerStepArgs,
secretName?: string
): k8s.V1Container {
const podContainer = new k8s.V1Container() const podContainer = new k8s.V1Container()
podContainer.name = JOB_CONTAINER_NAME podContainer.name = JOB_CONTAINER_NAME
podContainer.image = container.image podContainer.image = container.image

View File

@@ -1,4 +1,5 @@
import { Command, getInputFromStdin, prepareJobArgs } from 'hooklib' import { Command, getInputFromStdin, prepareJobArgs } from 'hooklib'
import * as core from '@actions/core'
import { import {
cleanupJob, cleanupJob,
prepareJob, prepareJob,
@@ -34,8 +35,7 @@ async function run(): Promise<void> {
throw new Error(`Command not recognized: ${command}`) throw new Error(`Command not recognized: ${command}`)
} }
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console core.error(JSON.stringify(error))
console.log(error)
exitCode = 1 exitCode = 1
} }
process.exitCode = exitCode process.exitCode = exitCode

View File

@@ -1,5 +1,5 @@
import * as k8s from '@kubernetes/client-node' import * as k8s from '@kubernetes/client-node'
import { ContainerInfo, PodPhase, Registry } from 'hooklib' import { ContainerInfo, Registry } from 'hooklib'
import * as stream from 'stream' import * as stream from 'stream'
import { import {
getJobPodName, getJobPodName,
@@ -9,6 +9,7 @@ import {
getVolumeClaimName, getVolumeClaimName,
RunnerInstanceLabel RunnerInstanceLabel
} from '../hooks/constants' } from '../hooks/constants'
import { PodPhase } from './utils'
const kc = new k8s.KubeConfig() const kc = new k8s.KubeConfig()
@@ -355,7 +356,7 @@ async function getPodPhase(podName: string): Promise<PodPhase> {
if (!pod.status?.phase || !podPhaseLookup.has(pod.status.phase)) { if (!pod.status?.phase || !podPhaseLookup.has(pod.status.phase)) {
return PodPhase.UNKNOWN return PodPhase.UNKNOWN
} }
return pod.status?.phase return pod.status?.phase as PodPhase
} }
async function isJobSucceeded(jobName: string): Promise<boolean> { async function isJobSucceeded(jobName: string): Promise<boolean> {

View File

@@ -70,3 +70,12 @@ export function containerVolumes(
return mounts return mounts
} }
export enum PodPhase {
PENDING = 'Pending',
RUNNING = 'Running',
SUCCEEDED = 'Succeeded',
FAILED = 'Failed',
UNKNOWN = 'Unknown',
COMPLETED = 'Completed'
}