mirror of
https://github.com/actions/runner-container-hooks.git
synced 2025-12-13 16:16:46 +00:00
fixed merge conflict, repaired paths in examples
This commit is contained in:
1
.github/workflows/build.yaml
vendored
1
.github/workflows/build.yaml
vendored
@@ -10,6 +10,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: helm/kind-action@v1.2.0
|
||||
- uses: actions/checkout@v3
|
||||
- run: npm install
|
||||
name: Install dependencies
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"args": {
|
||||
"container": {
|
||||
"image": "node:14.16",
|
||||
"workingDirectory": "/__w/thboop-test2/thboop-test2",
|
||||
"workingDirectory": "/__w/repo/repo",
|
||||
"createOptions": "--cpus 1",
|
||||
"environmentVariables": {
|
||||
"NODE_ENV": "development"
|
||||
@@ -24,37 +24,37 @@
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work",
|
||||
"targetVolumePath": "/__w",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/externals",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/externals",
|
||||
"targetVolumePath": "/__e",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work/_temp",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work/_temp",
|
||||
"targetVolumePath": "/__w/_temp",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work/_actions",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work/_actions",
|
||||
"targetVolumePath": "/__w/_actions",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work/_tool",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work/_tool",
|
||||
"targetVolumePath": "/__w/_tool",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work/_temp/_github_home",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work/_temp/_github_home",
|
||||
"targetVolumePath": "/github/home",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work/_temp/_github_workflow",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work/_temp/_github_workflow",
|
||||
"targetVolumePath": "/github/workflow",
|
||||
"readOnly": false
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"echo \"hello world2\""
|
||||
],
|
||||
"entryPoint": "bash",
|
||||
"workingDirectory": "/__w/thboop-test2/thboop-test2",
|
||||
"workingDirectory": "/__w/repo/repo",
|
||||
"createOptions": "--cpus 1",
|
||||
"environmentVariables": {
|
||||
"NODE_ENV": "development"
|
||||
@@ -34,27 +34,27 @@
|
||||
],
|
||||
"systemMountVolumes": [
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work",
|
||||
"targetVolumePath": "/__w",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/externals",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/externals",
|
||||
"targetVolumePath": "/__e",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work/_temp",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work/_temp",
|
||||
"targetVolumePath": "/__w/_temp",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work/_actions",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work/_actions",
|
||||
"targetVolumePath": "/__w/_actions",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work/_tool",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work/_tool",
|
||||
"targetVolumePath": "/__w/_tool",
|
||||
"readOnly": false
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolumePath": "//Users/thomas/git/runner/_layout/_work/_temp/_github_workflow",
|
||||
"sourceVolumePath": "/Users/thomas/git/runner/_layout/_work/_temp/_github_workflow",
|
||||
"targetVolumePath": "/github/workflow",
|
||||
"readOnly": false
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"/foo/bar",
|
||||
"bar/foo"
|
||||
],
|
||||
"workingDirectory": "/__w/thboop-test2/thboop-test2"
|
||||
"workingDirectory": "/__w/repo/repo"
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"doc": "docs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run test --prefix packages/docker",
|
||||
"test": "npm run test --prefix packages/docker && npm run test --prefix packages/k8s",
|
||||
"bootstrap": "npm install --prefix packages/hooklib && npm install --prefix packages/k8s && npm install --prefix packages/docker",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
|
||||
@@ -1 +1 @@
|
||||
jest.setTimeout(90000)
|
||||
jest.setTimeout(500000)
|
||||
|
||||
@@ -108,7 +108,6 @@ ENTRYPOINT [ "tail", "-f", "/dev/null" ]
|
||||
process.env.GITHUB_WORKSPACE = tmpOutputDir
|
||||
containerStepDataCopy.args.dockerfile = 'Dockerfile'
|
||||
containerStepDataCopy.args.context = '.'
|
||||
console.log(containerStepDataCopy.args)
|
||||
await expect(
|
||||
runContainerStep(containerStepDataCopy.args, resp.state)
|
||||
).resolves.not.toThrow()
|
||||
|
||||
@@ -76,7 +76,7 @@ export enum Protocol {
|
||||
export enum PodPhase {
|
||||
PENDING = 'Pending',
|
||||
RUNNING = 'Running',
|
||||
SUCCEEDED = 'Succeded',
|
||||
SUCCEEDED = 'Succeeded',
|
||||
FAILED = 'Failed',
|
||||
UNKNOWN = 'Unknown'
|
||||
}
|
||||
|
||||
@@ -6,7 +6,24 @@ This implementation provides a way to dynamically spin up jobs to run container
|
||||
## Pre-requisites
|
||||
Some things are expected to be set when using these hooks
|
||||
- The runner itself should be running in a pod, with a service account with the following permissions
|
||||
- The `ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER=true` should be set to true
|
||||
```
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/exec"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
```
|
||||
- The `ACTIONS_RUNNER_POD_NAME` env should be set to the name of the pod
|
||||
- The `ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER` env should be set to true to prevent the runner from running any jobs outside of a container
|
||||
- The runner pod should map a persistent volume claim into the `_work` directory
|
||||
- The `ACTIONS_RUNNER_CLAIM_NAME` should be set to the persistent volume claim that contains the runner's working directory
|
||||
- The `ACTIONS_RUNNER_CLAIM_NAME` env should be set to the persistent volume claim that contains the runner's working directory
|
||||
- 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
|
||||
- `GITHUB_WORKSPACE` is expected to be set to the workspace of the job
|
||||
|
||||
@@ -1 +1 @@
|
||||
jest.setTimeout(90000)
|
||||
jest.setTimeout(500000)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { podPrune } from '../k8s'
|
||||
import { pruneSecrets, prunePods } from '../k8s'
|
||||
|
||||
export async function cleanupJob(): Promise<void> {
|
||||
await podPrune()
|
||||
await prunePods()
|
||||
await pruneSecrets()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function getJobPodName(): string {
|
||||
export function getStepPodName(): string {
|
||||
return `${getRunnerPodName().substring(
|
||||
0,
|
||||
MAX_POD_NAME_LENGTH - ('-step'.length + STEP_POD_NAME_SUFFIX_LENGTH)
|
||||
MAX_POD_NAME_LENGTH - ('-step-'.length + STEP_POD_NAME_SUFFIX_LENGTH)
|
||||
)}-step-${uuidv4().substring(0, STEP_POD_NAME_SUFFIX_LENGTH)}`
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ export function getVolumeClaimName(): string {
|
||||
return name
|
||||
}
|
||||
|
||||
export function getSecretName(): string {
|
||||
return `${getRunnerPodName().substring(
|
||||
0,
|
||||
MAX_POD_NAME_LENGTH - ('-secret-'.length + STEP_POD_NAME_SUFFIX_LENGTH)
|
||||
)}-secret-${uuidv4().substring(0, STEP_POD_NAME_SUFFIX_LENGTH)}`
|
||||
}
|
||||
|
||||
const MAX_POD_NAME_LENGTH = 63
|
||||
const STEP_POD_NAME_SUFFIX_LENGTH = 8
|
||||
export const JOB_CONTAINER_NAME = 'job'
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
isAuthPermissionsOK,
|
||||
isPodContainerAlpine,
|
||||
namespace,
|
||||
podPrune,
|
||||
prunePods,
|
||||
requiredPermissions,
|
||||
waitForPodPhases
|
||||
} from '../k8s'
|
||||
@@ -29,7 +29,7 @@ export async function prepareJob(
|
||||
args: prepareJobArgs,
|
||||
responseFile
|
||||
): Promise<void> {
|
||||
await podPrune()
|
||||
await prunePods()
|
||||
if (!(await isAuthPermissionsOK())) {
|
||||
throw new Error(
|
||||
`The Service account needs the following permissions ${JSON.stringify(
|
||||
@@ -58,8 +58,8 @@ export async function prepareJob(
|
||||
try {
|
||||
createdPod = await createPod(container, services, args.registry)
|
||||
} catch (err) {
|
||||
await podPrune()
|
||||
throw new Error(`failed to create job pod: ${err}`)
|
||||
await prunePods()
|
||||
throw new Error(`failed to create job pod: ${JSON.stringify(err)}`)
|
||||
}
|
||||
|
||||
if (!createdPod?.metadata?.name) {
|
||||
@@ -73,7 +73,7 @@ export async function prepareJob(
|
||||
new Set([PodPhase.PENDING])
|
||||
)
|
||||
} catch (err) {
|
||||
await podPrune()
|
||||
await prunePods()
|
||||
throw new Error(`Pod failed to come online with error: ${err}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as core from '@actions/core'
|
||||
import { PodPhase } from 'hooklib'
|
||||
import {
|
||||
createJob,
|
||||
createSecretForEnvs,
|
||||
getContainerJobPodName,
|
||||
getPodLogs,
|
||||
getPodStatus,
|
||||
@@ -16,7 +17,13 @@ export async function runContainerStep(stepContainer): Promise<number> {
|
||||
if (stepContainer.dockerfile) {
|
||||
throw new Error('Building container actions is not currently supported')
|
||||
}
|
||||
const container = createPodSpec(stepContainer)
|
||||
let secretName: string | undefined = undefined
|
||||
if (stepContainer['environmentVariables']) {
|
||||
secretName = await createSecretForEnvs(
|
||||
stepContainer['environmentVariables']
|
||||
)
|
||||
}
|
||||
const container = createPodSpec(stepContainer, secretName)
|
||||
const job = await createJob(container)
|
||||
if (!job.metadata?.name) {
|
||||
throw new Error(
|
||||
@@ -25,12 +32,11 @@ export async function runContainerStep(stepContainer): Promise<number> {
|
||||
)} to have correctly set the metadata.name`
|
||||
)
|
||||
}
|
||||
|
||||
const podName = await getContainerJobPodName(job.metadata.name)
|
||||
await waitForPodPhases(
|
||||
podName,
|
||||
new Set([PodPhase.COMPLETED, PodPhase.RUNNING]),
|
||||
new Set([PodPhase.PENDING])
|
||||
new Set([PodPhase.COMPLETED, PodPhase.RUNNING, PodPhase.SUCCEEDED]),
|
||||
new Set([PodPhase.PENDING, PodPhase.UNKNOWN])
|
||||
)
|
||||
await getPodLogs(podName, JOB_CONTAINER_NAME)
|
||||
await waitForJobToComplete(job.metadata.name)
|
||||
@@ -40,28 +46,28 @@ export async function runContainerStep(stepContainer): Promise<number> {
|
||||
core.warning(`Can't determine container status`)
|
||||
return 0
|
||||
}
|
||||
|
||||
const exitCode =
|
||||
status.containerStatuses[status.containerStatuses.length - 1].state
|
||||
?.terminated?.exitCode
|
||||
return Number(exitCode) || 0
|
||||
}
|
||||
|
||||
function createPodSpec(container): k8s.V1Container {
|
||||
function createPodSpec(container, secretName?: string): k8s.V1Container {
|
||||
const podContainer = new k8s.V1Container()
|
||||
podContainer.name = JOB_CONTAINER_NAME
|
||||
podContainer.image = container.image
|
||||
if (container.entryPoint) {
|
||||
podContainer.command = [container.entryPoint, ...container.entryPointArgs]
|
||||
}
|
||||
|
||||
podContainer.env = []
|
||||
for (const [key, value] of Object.entries(
|
||||
container['environmentVariables']
|
||||
)) {
|
||||
if (value && key !== 'HOME') {
|
||||
podContainer.env.push({ name: key, value: value as string })
|
||||
}
|
||||
if (secretName) {
|
||||
podContainer.envFrom = [
|
||||
{
|
||||
secretRef: {
|
||||
name: secretName,
|
||||
optional: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
podContainer.volumeMounts = containerVolumes()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { RunScriptStepArgs } from 'hooklib'
|
||||
import { execPodStep } from '../k8s'
|
||||
import { JOB_CONTAINER_NAME } from './constants'
|
||||
import { getJobPodName, JOB_CONTAINER_NAME } from './constants'
|
||||
|
||||
export async function runScriptStep(
|
||||
args: RunScriptStepArgs,
|
||||
@@ -13,7 +13,7 @@ export async function runScriptStep(
|
||||
args.entryPointArgs,
|
||||
args.environmentVariables
|
||||
)
|
||||
await execPodStep(cb.command, state.jobPod, JOB_CONTAINER_NAME)
|
||||
await execPodStep(cb.command, getJobPodName(), JOB_CONTAINER_NAME)
|
||||
}
|
||||
|
||||
class CommandsBuilder {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as k8s from '@kubernetes/client-node'
|
||||
import { ContainerInfo, PodPhase, Registry } from 'hooklib'
|
||||
import * as stream from 'stream'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
getJobPodName,
|
||||
getRunnerPodName,
|
||||
getSecretName,
|
||||
getStepPodName,
|
||||
getVolumeClaimName,
|
||||
RunnerInstanceLabel
|
||||
} from '../hooks/constants'
|
||||
@@ -119,7 +120,7 @@ export async function createJob(
|
||||
job.apiVersion = 'batch/v1'
|
||||
job.kind = 'Job'
|
||||
job.metadata = new k8s.V1ObjectMeta()
|
||||
job.metadata.name = getJobPodName()
|
||||
job.metadata.name = getStepPodName()
|
||||
job.metadata.labels = { 'runner-pod': getRunnerPodName() }
|
||||
|
||||
job.spec = new k8s.V1JobSpec()
|
||||
@@ -173,7 +174,13 @@ export async function getContainerJobPodName(jobName: string): Promise<string> {
|
||||
}
|
||||
|
||||
export async function deletePod(podName: string): Promise<void> {
|
||||
await k8sApi.deleteNamespacedPod(podName, namespace())
|
||||
await k8sApi.deleteNamespacedPod(
|
||||
podName,
|
||||
namespace(),
|
||||
undefined,
|
||||
undefined,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
export async function execPodStep(
|
||||
@@ -244,12 +251,13 @@ export async function createDockerSecret(
|
||||
}
|
||||
}
|
||||
}
|
||||
const secretName = generateSecretName()
|
||||
const secretName = getSecretName()
|
||||
const secret = new k8s.V1Secret()
|
||||
secret.immutable = true
|
||||
secret.apiVersion = 'v1'
|
||||
secret.metadata = new k8s.V1ObjectMeta()
|
||||
secret.metadata.name = secretName
|
||||
secret.metadata.labels = { 'runner-pod': getRunnerPodName() }
|
||||
secret.kind = 'Secret'
|
||||
secret.data = {
|
||||
'.dockerconfigjson': Buffer.from(
|
||||
@@ -262,6 +270,50 @@ export async function createDockerSecret(
|
||||
return body
|
||||
}
|
||||
|
||||
export async function createSecretForEnvs(envs: {
|
||||
[key: string]: string
|
||||
}): Promise<string> {
|
||||
const secret = new k8s.V1Secret()
|
||||
const secretName = getSecretName()
|
||||
secret.immutable = true
|
||||
secret.apiVersion = 'v1'
|
||||
secret.metadata = new k8s.V1ObjectMeta()
|
||||
secret.metadata.name = secretName
|
||||
secret.metadata.labels = { 'runner-pod': getRunnerPodName() }
|
||||
secret.kind = 'Secret'
|
||||
secret.data = {}
|
||||
for (const [key, value] of Object.entries(envs)) {
|
||||
secret.data[key] = Buffer.from(value).toString('base64')
|
||||
}
|
||||
|
||||
await k8sApi.createNamespacedSecret(namespace(), secret)
|
||||
return secretName
|
||||
}
|
||||
|
||||
export async function deleteSecret(secretName: string): Promise<void> {
|
||||
await k8sApi.deleteNamespacedSecret(secretName, namespace())
|
||||
}
|
||||
|
||||
export async function pruneSecrets(): Promise<void> {
|
||||
const secretList = await k8sApi.listNamespacedSecret(
|
||||
namespace(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
new RunnerInstanceLabel().toString()
|
||||
)
|
||||
if (!secretList.body.items.length) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
secretList.body.items.map(
|
||||
secret => secret.metadata?.name && deleteSecret(secret.metadata.name)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export async function waitForPodPhases(
|
||||
podName: string,
|
||||
awaitingPhases: Set<PodPhase>,
|
||||
@@ -273,7 +325,6 @@ export async function waitForPodPhases(
|
||||
try {
|
||||
while (true) {
|
||||
phase = await getPodPhase(podName)
|
||||
|
||||
if (awaitingPhases.has(phase)) {
|
||||
return
|
||||
}
|
||||
@@ -340,7 +391,7 @@ export async function getPodLogs(
|
||||
await new Promise(resolve => r.on('close', () => resolve(null)))
|
||||
}
|
||||
|
||||
export async function podPrune(): Promise<void> {
|
||||
export async function prunePods(): Promise<void> {
|
||||
const podList = await k8sApi.listNamespacedPod(
|
||||
namespace(),
|
||||
undefined,
|
||||
@@ -454,10 +505,6 @@ export function namespace(): string {
|
||||
return context.namespace
|
||||
}
|
||||
|
||||
function generateSecretName(): string {
|
||||
return `github-secret-${uuidv4()}`
|
||||
}
|
||||
|
||||
function runnerName(): string {
|
||||
const name = process.env.ACTIONS_RUNNER_POD_NAME
|
||||
if (!name) {
|
||||
|
||||
@@ -52,9 +52,9 @@ export function containerVolumes(
|
||||
'absolute path volume mounts outside of the work folder are not supported'
|
||||
)
|
||||
}
|
||||
sourceVolumePath = userVolume.sourceVolumePath
|
||||
sourceVolumePath = userVolume.sourceVolumePath.slice(workspacePath.length)
|
||||
} else {
|
||||
sourceVolumePath = path.join(workspacePath, userVolume.sourceVolumePath)
|
||||
sourceVolumePath = userVolume.sourceVolumePath
|
||||
}
|
||||
|
||||
mounts.push({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { prepareJob, cleanupJob } from '../src/hooks'
|
||||
import { TestTempOutput } from './test-setup'
|
||||
import { TestHelper } from './test-setup'
|
||||
|
||||
let testTempOutput: TestTempOutput
|
||||
let testHelper: TestHelper
|
||||
|
||||
const prepareJobJsonPath = path.resolve(
|
||||
`${__dirname}/../../../examples/prepare-job.json`
|
||||
@@ -16,16 +16,15 @@ describe('Cleanup Job', () => {
|
||||
const prepareJobJson = fs.readFileSync(prepareJobJsonPath)
|
||||
let prepareJobData = JSON.parse(prepareJobJson.toString())
|
||||
|
||||
testTempOutput = new TestTempOutput()
|
||||
testTempOutput.initialize()
|
||||
prepareJobOutputFilePath = testTempOutput.createFile(
|
||||
'prepare-job-output.json'
|
||||
)
|
||||
testHelper = new TestHelper()
|
||||
await testHelper.initialize()
|
||||
prepareJobOutputFilePath = testHelper.createFile('prepare-job-output.json')
|
||||
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
||||
})
|
||||
it('should not throw', async () => {
|
||||
const outputJson = fs.readFileSync(prepareJobOutputFilePath)
|
||||
const outputData = JSON.parse(outputJson.toString())
|
||||
await expect(cleanupJob()).resolves.not.toThrow()
|
||||
})
|
||||
afterEach(async () => {
|
||||
await testHelper.cleanup()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,38 +6,36 @@ import {
|
||||
runContainerStep,
|
||||
runScriptStep
|
||||
} from '../src/hooks'
|
||||
import { TestTempOutput } from './test-setup'
|
||||
import { TestHelper } from './test-setup'
|
||||
|
||||
jest.useRealTimers()
|
||||
|
||||
let testTempOutput: TestTempOutput
|
||||
let testHelper: TestHelper
|
||||
|
||||
const prepareJobJsonPath = path.resolve(
|
||||
`${__dirname}/../../../../examples/prepare-job.json`
|
||||
`${__dirname}/../../../examples/prepare-job.json`
|
||||
)
|
||||
const runScriptStepJsonPath = path.resolve(
|
||||
`${__dirname}/../../../../examples/run-script-step.json`
|
||||
`${__dirname}/../../../examples/run-script-step.json`
|
||||
)
|
||||
let runContainerStepJsonPath = path.resolve(
|
||||
`${__dirname}/../../../../examples/run-container-step.json`
|
||||
`${__dirname}/../../../examples/run-container-step.json`
|
||||
)
|
||||
|
||||
let prepareJobData: any
|
||||
|
||||
let prepareJobOutputFilePath: string
|
||||
describe('e2e', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const prepareJobJson = fs.readFileSync(prepareJobJsonPath)
|
||||
prepareJobData = JSON.parse(prepareJobJson.toString())
|
||||
|
||||
testTempOutput = new TestTempOutput()
|
||||
testTempOutput.initialize()
|
||||
prepareJobOutputFilePath = testTempOutput.createFile(
|
||||
'prepare-job-output.json'
|
||||
)
|
||||
testHelper = new TestHelper()
|
||||
await testHelper.initialize()
|
||||
prepareJobOutputFilePath = testHelper.createFile('prepare-job-output.json')
|
||||
})
|
||||
afterEach(async () => {
|
||||
testTempOutput.cleanup()
|
||||
await testHelper.cleanup()
|
||||
})
|
||||
it('should prepare job, run script step, run container step then cleanup without errors', async () => {
|
||||
await expect(
|
||||
|
||||
@@ -2,11 +2,11 @@ import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { cleanupJob } from '../src/hooks'
|
||||
import { prepareJob } from '../src/hooks/prepare-job'
|
||||
import { TestTempOutput } from './test-setup'
|
||||
import { TestHelper } from './test-setup'
|
||||
|
||||
jest.useRealTimers()
|
||||
|
||||
let testTempOutput: TestTempOutput
|
||||
let testHelper: TestHelper
|
||||
|
||||
const prepareJobJsonPath = path.resolve(
|
||||
`${__dirname}/../../../examples/prepare-job.json`
|
||||
@@ -16,21 +16,17 @@ let prepareJobData: any
|
||||
let prepareJobOutputFilePath: string
|
||||
|
||||
describe('Prepare job', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const prepareJobJson = fs.readFileSync(prepareJobJsonPath)
|
||||
prepareJobData = JSON.parse(prepareJobJson.toString())
|
||||
|
||||
testTempOutput = new TestTempOutput()
|
||||
testTempOutput.initialize()
|
||||
prepareJobOutputFilePath = testTempOutput.createFile(
|
||||
'prepare-job-output.json'
|
||||
)
|
||||
testHelper = new TestHelper()
|
||||
await testHelper.initialize()
|
||||
prepareJobOutputFilePath = testHelper.createFile('prepare-job-output.json')
|
||||
})
|
||||
afterEach(async () => {
|
||||
const outputJson = fs.readFileSync(prepareJobOutputFilePath)
|
||||
const outputData = JSON.parse(outputJson.toString())
|
||||
await cleanupJob()
|
||||
testTempOutput.cleanup()
|
||||
await testHelper.cleanup()
|
||||
})
|
||||
|
||||
it('should not throw exception', async () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { TestTempOutput } from './test-setup'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { runContainerStep } from '../src/hooks'
|
||||
import * as fs from 'fs'
|
||||
import { TestHelper } from './test-setup'
|
||||
|
||||
jest.useRealTimers()
|
||||
|
||||
let testTempOutput: TestTempOutput
|
||||
let testHelper: TestHelper
|
||||
|
||||
let runContainerStepJsonPath = path.resolve(
|
||||
`${__dirname}/../../../examples/run-container-step.json`
|
||||
@@ -14,14 +14,18 @@ let runContainerStepJsonPath = path.resolve(
|
||||
let runContainerStepData: any
|
||||
|
||||
describe('Run container step', () => {
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
const content = fs.readFileSync(runContainerStepJsonPath)
|
||||
runContainerStepData = JSON.parse(content.toString())
|
||||
process.env.RUNNER_NAME = 'testjob'
|
||||
testHelper = new TestHelper()
|
||||
await testHelper.initialize()
|
||||
})
|
||||
it('should not throw', async () => {
|
||||
await expect(
|
||||
runContainerStep(runContainerStepData.args)
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
afterEach(async () => {
|
||||
await testHelper.cleanup()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { prepareJob, cleanupJob, runScriptStep } from '../src/hooks'
|
||||
import { TestTempOutput } from './test-setup'
|
||||
import { TestHelper } from './test-setup'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
jest.useRealTimers()
|
||||
|
||||
let testTempOutput: TestTempOutput
|
||||
let testHelper: TestHelper
|
||||
|
||||
const prepareJobJsonPath = path.resolve(
|
||||
`${__dirname}/../../../examples/prepare-job.json`
|
||||
@@ -19,13 +19,10 @@ describe('Run script step', () => {
|
||||
beforeEach(async () => {
|
||||
const prepareJobJson = fs.readFileSync(prepareJobJsonPath)
|
||||
prepareJobData = JSON.parse(prepareJobJson.toString())
|
||||
console.log(prepareJobData)
|
||||
|
||||
testTempOutput = new TestTempOutput()
|
||||
testTempOutput.initialize()
|
||||
prepareJobOutputFilePath = testTempOutput.createFile(
|
||||
'prepare-job-output.json'
|
||||
)
|
||||
testHelper = new TestHelper()
|
||||
await testHelper.initialize()
|
||||
prepareJobOutputFilePath = testHelper.createFile('prepare-job-output.json')
|
||||
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
||||
const outputContent = fs.readFileSync(prepareJobOutputFilePath)
|
||||
prepareJobOutputData = JSON.parse(outputContent.toString())
|
||||
@@ -33,7 +30,7 @@ describe('Run script step', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupJob()
|
||||
testTempOutput.cleanup()
|
||||
await testHelper.cleanup()
|
||||
})
|
||||
|
||||
// NOTE: To use this test, do kubectl apply -f podspec.yaml (from podspec examples)
|
||||
@@ -42,8 +39,8 @@ describe('Run script step', () => {
|
||||
|
||||
it('should not throw an exception', async () => {
|
||||
const args = {
|
||||
entryPointArgs: ['echo "test"'],
|
||||
entryPoint: '/bin/bash',
|
||||
entryPointArgs: ['-c', 'echo "test"'],
|
||||
entryPoint: 'bash',
|
||||
environmentVariables: {
|
||||
NODE_ENV: 'development'
|
||||
},
|
||||
|
||||
@@ -1,20 +1,69 @@
|
||||
import * as k8s from '@kubernetes/client-node'
|
||||
import * as fs from 'fs'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export class TestTempOutput {
|
||||
const kc = new k8s.KubeConfig()
|
||||
|
||||
kc.loadFromDefault()
|
||||
|
||||
const k8sApi = kc.makeApiClient(k8s.CoreV1Api)
|
||||
const k8sStorageApi = kc.makeApiClient(k8s.StorageV1Api)
|
||||
|
||||
export class TestHelper {
|
||||
private tempDirPath: string
|
||||
private podName: string
|
||||
constructor() {
|
||||
this.tempDirPath = `${__dirname}/_temp/${uuidv4()}`
|
||||
this.tempDirPath = `${__dirname}/_temp/runner`
|
||||
this.podName = uuidv4().replace(/-/g, '')
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
fs.mkdirSync(this.tempDirPath, { recursive: true })
|
||||
public async initialize(): Promise<void> {
|
||||
process.env['ACTIONS_RUNNER_POD_NAME'] = `${this.podName}`
|
||||
process.env['ACTIONS_RUNNER_CLAIM_NAME'] = `${this.podName}-work`
|
||||
process.env['RUNNER_WORKSPACE'] = `${this.tempDirPath}/work/repo`
|
||||
process.env['GITHUB_WORKSPACE'] = `${this.tempDirPath}/work/repo/repo`
|
||||
process.env['ACTIONS_RUNNER_KUBERNETES_NAMESPACE'] = 'default'
|
||||
|
||||
await this.cleanupK8sResources()
|
||||
try {
|
||||
await this.createTestVolume()
|
||||
await this.createTestJobPod()
|
||||
} catch {}
|
||||
fs.mkdirSync(`${this.tempDirPath}/work/repo/repo`, { recursive: true })
|
||||
fs.mkdirSync(`${this.tempDirPath}/externals`, { recursive: true })
|
||||
}
|
||||
|
||||
public cleanup(): void {
|
||||
fs.rmSync(this.tempDirPath, { recursive: true })
|
||||
public async cleanup(): Promise<void> {
|
||||
try {
|
||||
await this.cleanupK8sResources()
|
||||
fs.rmSync(this.tempDirPath, { recursive: true })
|
||||
} catch {}
|
||||
}
|
||||
public async cleanupK8sResources() {
|
||||
await k8sApi
|
||||
.deleteNamespacedPersistentVolumeClaim(
|
||||
`${this.podName}-work`,
|
||||
'default',
|
||||
undefined,
|
||||
undefined,
|
||||
0
|
||||
)
|
||||
.catch(e => {})
|
||||
await k8sApi.deletePersistentVolume(`${this.podName}-pv`).catch(e => {})
|
||||
await k8sStorageApi.deleteStorageClass('local-storage').catch(e => {})
|
||||
await k8sApi
|
||||
.deleteNamespacedPod(this.podName, 'default', undefined, undefined, 0)
|
||||
.catch(e => {})
|
||||
await k8sApi
|
||||
.deleteNamespacedPod(
|
||||
`${this.podName}-workflow`,
|
||||
'default',
|
||||
undefined,
|
||||
undefined,
|
||||
0
|
||||
)
|
||||
.catch(e => {})
|
||||
}
|
||||
|
||||
public createFile(fileName?: string): string {
|
||||
const filePath = `${this.tempDirPath}/${fileName || uuidv4()}`
|
||||
fs.writeFileSync(filePath, '')
|
||||
@@ -25,4 +74,69 @@ export class TestTempOutput {
|
||||
const filePath = `${this.tempDirPath}/${fileName}`
|
||||
fs.rmSync(filePath)
|
||||
}
|
||||
|
||||
public async createTestJobPod() {
|
||||
const container = {
|
||||
name: 'nginx',
|
||||
image: 'nginx:latest',
|
||||
imagePullPolicy: 'IfNotPresent'
|
||||
} as k8s.V1Container
|
||||
|
||||
const pod: k8s.V1Pod = {
|
||||
metadata: {
|
||||
name: this.podName
|
||||
},
|
||||
spec: {
|
||||
restartPolicy: 'Never',
|
||||
containers: [container]
|
||||
}
|
||||
} as k8s.V1Pod
|
||||
await k8sApi.createNamespacedPod('default', pod)
|
||||
}
|
||||
|
||||
public async createTestVolume() {
|
||||
var sc: k8s.V1StorageClass = {
|
||||
metadata: {
|
||||
name: 'local-storage'
|
||||
},
|
||||
provisioner: 'kubernetes.io/no-provisioner',
|
||||
volumeBindingMode: 'Immediate'
|
||||
}
|
||||
await k8sStorageApi.createStorageClass(sc)
|
||||
|
||||
var volume: k8s.V1PersistentVolume = {
|
||||
metadata: {
|
||||
name: `${this.podName}-pv`
|
||||
},
|
||||
spec: {
|
||||
storageClassName: 'local-storage',
|
||||
capacity: {
|
||||
storage: '2Gi'
|
||||
},
|
||||
volumeMode: 'Filesystem',
|
||||
accessModes: ['ReadWriteOnce'],
|
||||
hostPath: {
|
||||
path: this.tempDirPath
|
||||
}
|
||||
}
|
||||
}
|
||||
await k8sApi.createPersistentVolume(volume)
|
||||
var volumeClaim: k8s.V1PersistentVolumeClaim = {
|
||||
metadata: {
|
||||
name: `${this.podName}-work`
|
||||
},
|
||||
spec: {
|
||||
accessModes: ['ReadWriteOnce'],
|
||||
volumeMode: 'Filesystem',
|
||||
storageClassName: 'local-storage',
|
||||
volumeName: `${this.podName}-pv`,
|
||||
resources: {
|
||||
requests: {
|
||||
storage: '1Gi'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await k8sApi.createNamespacedPersistentVolumeClaim('default', volumeClaim)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user