mirror of
https://github.com/docker/build-push-action.git
synced 2025-05-06 21:49:33 +02:00
*: refactor methods to support mocking
Additionally, write some tests to ensure the driver method `startBlacksmithBuilder` handles all exceptions correctly in both nofallback=true and nofallback=false configurations.
This commit is contained in:
parent
15e5beff2d
commit
c71ad2dbef
15 changed files with 712 additions and 1380 deletions
242
src/setup_builder.ts
Normal file
242
src/setup_builder.ts
Normal file
|
@ -0,0 +1,242 @@
|
|||
import * as fs from 'fs';
|
||||
import * as core from '@actions/core';
|
||||
import {AxiosError} from 'axios';
|
||||
import {exec} from 'child_process';
|
||||
import {promisify} from 'util';
|
||||
import * as TOML from '@iarna/toml';
|
||||
import {Inputs} from './context';
|
||||
import * as reporter from './reporter';
|
||||
import * as utils from './utils';
|
||||
|
||||
const mountPoint = '/var/lib/buildkit';
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function maybeFormatBlockDevice(device: string): Promise<string> {
|
||||
try {
|
||||
// Check if device is formatted with ext4
|
||||
try {
|
||||
const {stdout} = await execAsync(`sudo blkid -o value -s TYPE ${device}`);
|
||||
if (stdout.trim() === 'ext4') {
|
||||
core.debug(`Device ${device} is already formatted with ext4`);
|
||||
try {
|
||||
// Run resize2fs to ensure filesystem uses full block device
|
||||
await execAsync(`sudo resize2fs -f ${device}`);
|
||||
core.debug(`Resized ext4 filesystem on ${device}`);
|
||||
} catch (error) {
|
||||
core.warning(`Error resizing ext4 filesystem on ${device}: ${error}`);
|
||||
}
|
||||
return device;
|
||||
}
|
||||
} catch (error) {
|
||||
// blkid returns non-zero if no filesystem found, which is fine
|
||||
core.debug(`No filesystem found on ${device}, will format it`);
|
||||
}
|
||||
|
||||
// Format device with ext4
|
||||
core.debug(`Formatting device ${device} with ext4`);
|
||||
await execAsync(`sudo mkfs.ext4 -m0 -Enodiscard,lazy_itable_init=1,lazy_journal_init=1 -F ${device}`);
|
||||
core.debug(`Successfully formatted ${device} with ext4`);
|
||||
return device;
|
||||
} catch (error) {
|
||||
core.error(`Failed to format device ${device}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function getNumCPUs(): Promise<number> {
|
||||
try {
|
||||
const {stdout} = await execAsync('sudo nproc');
|
||||
return parseInt(stdout.trim());
|
||||
} catch (error) {
|
||||
core.warning('Failed to get CPU count, defaulting to 1:', error);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeBuildkitdTomlFile(parallelism: number, device: string): Promise<void> {
|
||||
const diskSize = await getDiskSize(device);
|
||||
const jsonConfig: TOML.JsonMap = {
|
||||
root: '/var/lib/buildkit',
|
||||
grpc: {
|
||||
address: ['unix:///run/buildkit/buildkitd.sock']
|
||||
},
|
||||
registry: {
|
||||
'docker.io': {
|
||||
mirrors: ['http://192.168.127.1:5000'],
|
||||
http: true,
|
||||
insecure: true
|
||||
},
|
||||
'192.168.127.1:5000': {
|
||||
http: true,
|
||||
insecure: true
|
||||
}
|
||||
},
|
||||
worker: {
|
||||
oci: {
|
||||
enabled: true,
|
||||
gc: true,
|
||||
gckeepstorage: diskSize.toString(),
|
||||
'max-parallelism': parallelism,
|
||||
snapshotter: 'overlayfs',
|
||||
gcpolicy: [
|
||||
{
|
||||
all: true,
|
||||
keepDuration: 1209600
|
||||
},
|
||||
{
|
||||
all: true,
|
||||
keepBytes: diskSize.toString()
|
||||
}
|
||||
]
|
||||
},
|
||||
containerd: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tomlString = TOML.stringify(jsonConfig);
|
||||
|
||||
try {
|
||||
await fs.promises.writeFile('buildkitd.toml', tomlString);
|
||||
core.debug(`TOML configuration is ${tomlString}`);
|
||||
} catch (err) {
|
||||
core.warning('error writing TOML configuration:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function startBuildkitd(parallelism: number, device: string): Promise<string> {
|
||||
try {
|
||||
await writeBuildkitdTomlFile(parallelism, device);
|
||||
await execAsync('sudo mkdir -p /run/buildkit');
|
||||
await execAsync('sudo chmod 755 /run/buildkit');
|
||||
const addr = 'unix:///run/buildkit/buildkitd.sock';
|
||||
const {stdout: startStdout, stderr: startStderr} = await execAsync(
|
||||
`sudo nohup buildkitd --debug --addr ${addr} --allow-insecure-entitlement security.insecure --config=buildkitd.toml --allow-insecure-entitlement network.host > buildkitd.log 2>&1 &`
|
||||
);
|
||||
|
||||
if (startStderr) {
|
||||
throw new Error(`error starting buildkitd service: ${startStderr}`);
|
||||
}
|
||||
core.debug(`buildkitd daemon started successfully ${startStdout}`);
|
||||
|
||||
const {stderr} = await execAsync(`pgrep -f buildkitd`);
|
||||
if (stderr) {
|
||||
throw new Error(`error finding buildkitd PID: ${stderr}`);
|
||||
}
|
||||
return addr;
|
||||
} catch (error) {
|
||||
core.error('failed to start buildkitd daemon:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function getDiskSize(device: string): Promise<number> {
|
||||
try {
|
||||
const {stdout} = await execAsync(`sudo lsblk -b -n -o SIZE ${device}`);
|
||||
const sizeInBytes = parseInt(stdout.trim(), 10);
|
||||
if (isNaN(sizeInBytes)) {
|
||||
throw new Error('Failed to parse disk size');
|
||||
}
|
||||
return sizeInBytes;
|
||||
} catch (error) {
|
||||
console.error(`Error getting disk size: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function getStickyDisk(retryCondition: (error: AxiosError) => boolean, options?: {signal?: AbortSignal}): Promise<{expose_id: string; device: string}> {
|
||||
const client = await utils.getBlacksmithAgentClient();
|
||||
const formData = new FormData();
|
||||
// TODO(adityamaru): Support a stickydisk-per-build flag that will namespace the stickydisks by Dockerfile.
|
||||
// For now, we'll use the repo name as the stickydisk key.
|
||||
const repoName = process.env.GITHUB_REPO_NAME || '';
|
||||
if (repoName === '') {
|
||||
throw new Error('GITHUB_REPO_NAME is not set');
|
||||
}
|
||||
formData.append('stickyDiskKey', repoName);
|
||||
formData.append('region', process.env.BLACKSMITH_REGION || 'eu-central');
|
||||
formData.append('installationModelID', process.env.BLACKSMITH_INSTALLATION_MODEL_ID || '');
|
||||
formData.append('vmID', process.env.VM_ID || '');
|
||||
core.debug(`Getting sticky disk for ${repoName}`);
|
||||
core.debug('FormData contents:');
|
||||
for (const pair of formData.entries()) {
|
||||
core.debug(`${pair[0]}: ${pair[1]}`);
|
||||
}
|
||||
const response = await reporter.getWithRetry(client, '/stickydisks', formData, retryCondition, options);
|
||||
// For backward compatibility, if expose_id is set, return it
|
||||
if (response.data?.expose_id && response.data?.disk_identifier) {
|
||||
return {expose_id: response.data.expose_id, device: response.data.disk_identifier};
|
||||
}
|
||||
return {expose_id: '', device: ''};
|
||||
}
|
||||
|
||||
|
||||
// getBuilderAddr mounts a sticky disk for the entity, sets up buildkitd on top of it
|
||||
// and returns the address to the builder.
|
||||
// If it is unable to do so because of a timeout or an error it returns null.
|
||||
export async function getBuilderAddr(inputs: Inputs, dockerfilePath: string): Promise<{addr: string | null; buildId?: string | null; exposeId: string}> {
|
||||
try {
|
||||
const retryCondition = (error: AxiosError) => (error.response?.status ? error.response.status >= 500 : error.code === 'ECONNRESET');
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
let buildResponse: {docker_build_id: string} | null = null;
|
||||
let exposeId: string = '';
|
||||
let device: string = '';
|
||||
try {
|
||||
const stickyDiskResponse = await getStickyDisk(retryCondition, {signal: controller.signal});
|
||||
exposeId = stickyDiskResponse.expose_id;
|
||||
device = stickyDiskResponse.device;
|
||||
if (device === '') {
|
||||
// TODO(adityamaru): Remove this once all of our VM agents are returning the device in the stickydisk response.
|
||||
device = '/dev/vdb';
|
||||
}
|
||||
clearTimeout(timeoutId);
|
||||
await maybeFormatBlockDevice(device);
|
||||
buildResponse = await reporter.reportBuild(dockerfilePath);
|
||||
await execAsync(`sudo mkdir -p ${mountPoint}`);
|
||||
await execAsync(`sudo mount ${device} ${mountPoint}`);
|
||||
core.debug(`${device} has been mounted to ${mountPoint}`);
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
return {addr: null, exposeId: ''};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
core.debug('Successfully obtained sticky disk, proceeding to start buildkitd');
|
||||
|
||||
// Start buildkitd.
|
||||
const parallelism = await getNumCPUs();
|
||||
const buildkitdAddr = await startBuildkitd(parallelism, device);
|
||||
core.debug(`buildkitd daemon started at addr ${buildkitdAddr}`);
|
||||
// Change permissions on the buildkitd socket to allow non-root access
|
||||
const startTime = Date.now();
|
||||
const timeout = 5000; // 5 seconds in milliseconds
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
if (fs.existsSync('/run/buildkit/buildkitd.sock')) {
|
||||
// Change permissions on the buildkitd socket to allow non-root access
|
||||
await execAsync(`sudo chmod 666 /run/buildkit/buildkitd.sock`);
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100)); // Poll every 100ms
|
||||
}
|
||||
|
||||
if (!fs.existsSync('/run/buildkit/buildkitd.sock')) {
|
||||
throw new Error('buildkitd socket not found after 5s timeout');
|
||||
}
|
||||
return {addr: buildkitdAddr, buildId: buildResponse?.docker_build_id, exposeId: exposeId};
|
||||
} catch (error) {
|
||||
if ((error as AxiosError).response && (error as AxiosError).response!.status === 404) {
|
||||
if (!inputs.nofallback) {
|
||||
core.warning('No builder instances were available, falling back to a local build');
|
||||
}
|
||||
} else {
|
||||
core.warning(`Error in getBuildkitdAddr: ${(error as Error).message}`);
|
||||
}
|
||||
return {addr: null, exposeId: ''};
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue