mirror of
https://github.com/actions/runner-container-hooks.git
synced 2025-12-15 01:06:43 +00:00
Pass secrets more securely for container action
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { podPrune } from '../k8s'
|
import { pruneSecrets, prunePods } from '../k8s'
|
||||||
|
|
||||||
export async function cleanupJob(): Promise<void> {
|
export async function cleanupJob(): Promise<void> {
|
||||||
await podPrune()
|
await prunePods()
|
||||||
|
await pruneSecrets()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function getJobPodName(): string {
|
|||||||
export function getStepPodName(): string {
|
export function getStepPodName(): string {
|
||||||
return `${getRunnerPodName().substring(
|
return `${getRunnerPodName().substring(
|
||||||
0,
|
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)}`
|
)}-step-${uuidv4().substring(0, STEP_POD_NAME_SUFFIX_LENGTH)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +34,13 @@ export function getVolumeClaimName(): string {
|
|||||||
return name
|
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 MAX_POD_NAME_LENGTH = 63
|
||||||
const STEP_POD_NAME_SUFFIX_LENGTH = 8
|
const STEP_POD_NAME_SUFFIX_LENGTH = 8
|
||||||
export const JOB_CONTAINER_NAME = 'job'
|
export const JOB_CONTAINER_NAME = 'job'
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
isAuthPermissionsOK,
|
isAuthPermissionsOK,
|
||||||
isPodContainerAlpine,
|
isPodContainerAlpine,
|
||||||
namespace,
|
namespace,
|
||||||
podPrune,
|
prunePods,
|
||||||
requiredPermissions,
|
requiredPermissions,
|
||||||
waitForPodPhases
|
waitForPodPhases
|
||||||
} from '../k8s'
|
} from '../k8s'
|
||||||
@@ -29,7 +29,7 @@ export async function prepareJob(
|
|||||||
args: prepareJobArgs,
|
args: prepareJobArgs,
|
||||||
responseFile
|
responseFile
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await podPrune()
|
await prunePods()
|
||||||
if (!(await isAuthPermissionsOK())) {
|
if (!(await isAuthPermissionsOK())) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The Service account needs the following permissions ${JSON.stringify(
|
`The Service account needs the following permissions ${JSON.stringify(
|
||||||
@@ -58,7 +58,7 @@ export async function prepareJob(
|
|||||||
try {
|
try {
|
||||||
createdPod = await createPod(container, services, args.registry)
|
createdPod = await createPod(container, services, args.registry)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await podPrune()
|
await prunePods()
|
||||||
throw new Error(`failed to create job pod: ${JSON.stringify(err)}`)
|
throw new Error(`failed to create job pod: ${JSON.stringify(err)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export async function prepareJob(
|
|||||||
new Set([PodPhase.PENDING])
|
new Set([PodPhase.PENDING])
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await podPrune()
|
await prunePods()
|
||||||
throw new Error(`Pod failed to come online with error: ${err}`)
|
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 { PodPhase } from 'hooklib'
|
||||||
import {
|
import {
|
||||||
createJob,
|
createJob,
|
||||||
|
createSecretForEnvs,
|
||||||
getContainerJobPodName,
|
getContainerJobPodName,
|
||||||
getPodLogs,
|
getPodLogs,
|
||||||
getPodStatus,
|
getPodStatus,
|
||||||
@@ -16,7 +17,13 @@ export async function runContainerStep(stepContainer): 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')
|
||||||
}
|
}
|
||||||
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)
|
const job = await createJob(container)
|
||||||
if (!job.metadata?.name) {
|
if (!job.metadata?.name) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -39,28 +46,28 @@ export async function runContainerStep(stepContainer): Promise<number> {
|
|||||||
core.warning(`Can't determine container status`)
|
core.warning(`Can't determine container status`)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
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) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPodSpec(container): k8s.V1Container {
|
function createPodSpec(container, 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
|
||||||
if (container.entryPoint) {
|
if (container.entryPoint) {
|
||||||
podContainer.command = [container.entryPoint, ...container.entryPointArgs]
|
podContainer.command = [container.entryPoint, ...container.entryPointArgs]
|
||||||
}
|
}
|
||||||
|
if (secretName) {
|
||||||
podContainer.env = []
|
podContainer.envFrom = [
|
||||||
for (const [key, value] of Object.entries(
|
{
|
||||||
container['environmentVariables']
|
secretRef: {
|
||||||
)) {
|
name: secretName,
|
||||||
if (value && key !== 'HOME') {
|
optional: false
|
||||||
podContainer.env.push({ name: key, value: value as string })
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
podContainer.volumeMounts = containerVolumes()
|
podContainer.volumeMounts = containerVolumes()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as k8s from '@kubernetes/client-node'
|
import * as k8s from '@kubernetes/client-node'
|
||||||
import { ContainerInfo, PodPhase, Registry } from 'hooklib'
|
import { ContainerInfo, PodPhase, Registry } from 'hooklib'
|
||||||
import * as stream from 'stream'
|
import * as stream from 'stream'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import {
|
import {
|
||||||
getJobPodName,
|
getJobPodName,
|
||||||
getRunnerPodName,
|
getRunnerPodName,
|
||||||
|
getSecretName,
|
||||||
getStepPodName,
|
getStepPodName,
|
||||||
getVolumeClaimName,
|
getVolumeClaimName,
|
||||||
RunnerInstanceLabel
|
RunnerInstanceLabel
|
||||||
@@ -251,12 +251,13 @@ export async function createDockerSecret(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const secretName = generateSecretName()
|
const secretName = getSecretName()
|
||||||
const secret = new k8s.V1Secret()
|
const secret = new k8s.V1Secret()
|
||||||
secret.immutable = true
|
secret.immutable = true
|
||||||
secret.apiVersion = 'v1'
|
secret.apiVersion = 'v1'
|
||||||
secret.metadata = new k8s.V1ObjectMeta()
|
secret.metadata = new k8s.V1ObjectMeta()
|
||||||
secret.metadata.name = secretName
|
secret.metadata.name = secretName
|
||||||
|
secret.metadata.labels = { 'runner-pod': getRunnerPodName() }
|
||||||
secret.kind = 'Secret'
|
secret.kind = 'Secret'
|
||||||
secret.data = {
|
secret.data = {
|
||||||
'.dockerconfigjson': Buffer.from(
|
'.dockerconfigjson': Buffer.from(
|
||||||
@@ -269,6 +270,53 @@ export async function createDockerSecret(
|
|||||||
return body
|
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')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await k8sApi.createNamespacedSecret(namespace(), secret)
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
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(
|
export async function waitForPodPhases(
|
||||||
podName: string,
|
podName: string,
|
||||||
awaitingPhases: Set<PodPhase>,
|
awaitingPhases: Set<PodPhase>,
|
||||||
@@ -346,7 +394,7 @@ export async function getPodLogs(
|
|||||||
await new Promise(resolve => r.on('close', () => resolve(null)))
|
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(
|
const podList = await k8sApi.listNamespacedPod(
|
||||||
namespace(),
|
namespace(),
|
||||||
undefined,
|
undefined,
|
||||||
@@ -460,10 +508,6 @@ export function namespace(): string {
|
|||||||
return context.namespace
|
return context.namespace
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSecretName(): string {
|
|
||||||
return `github-secret-${uuidv4()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function runnerName(): string {
|
function runnerName(): string {
|
||||||
const name = process.env.ACTIONS_RUNNER_POD_NAME
|
const name = process.env.ACTIONS_RUNNER_POD_NAME
|
||||||
if (!name) {
|
if (!name) {
|
||||||
|
|||||||
@@ -27,7 +27,5 @@ describe('Run container step', () => {
|
|||||||
})
|
})
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await testHelper.cleanup()
|
await testHelper.cleanup()
|
||||||
// wait for the job cleanup
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 300 * 1000))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user