Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 31 additions & 20 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,26 @@ const mirror_telemetry_1 = __nccwpck_require__(7185);
const AGENT_RPC_TIMEOUT_MS = 45000;
const MOUNT_BASE = '/blacksmith-git-mirror';
const MIRROR_VERSION = 'v1';
/**
* Argv for a command that needs root. Job containers usually run as root
* and often ship without sudo, so the prefix is only added when needed.
*/
function asRoot(cmd, args) {
var _a;
return ((_a = process.getuid) === null || _a === void 0 ? void 0 : _a.call(process)) === 0 ? [cmd, ...args] : ['sudo', cmd, ...args];
}
function execAsRoot(cmd, args) {
return __awaiter(this, void 0, void 0, function* () {
const [file, ...rest] = asRoot(cmd, args);
return exec.exec(file, rest);
});
}
function getExecOutputAsRoot(cmd, args, options) {
return __awaiter(this, void 0, void 0, function* () {
const [file, ...rest] = asRoot(cmd, args);
return exec.getExecOutput(file, rest, options);
});
}
// A sync that doesn't finish within this window is abandoned (single
// attempt, no retries): sync failure is soft - the workspace is populated
// from the mirror's last good state plus a targeted fetch of the job's own
Expand Down Expand Up @@ -238,7 +258,7 @@ function waitForNonZeroDeviceSize(device, timeoutMs) {
return __awaiter(this, void 0, void 0, function* () {
const deadline = Date.now() + timeoutMs;
for (;;) {
const result = yield exec.getExecOutput('sudo', ['blockdev', '--getsize64', device], { ignoreReturnCode: true, silent: true });
const result = yield getExecOutputAsRoot('blockdev', ['--getsize64', device], { ignoreReturnCode: true, silent: true });
if (result.exitCode === 0) {
const size = parseInt(result.stdout.trim(), 10);
if (!isNaN(size) && size > 0) {
Expand All @@ -258,14 +278,14 @@ function waitForNonZeroDeviceSize(device, timeoutMs) {
function maybeFormatDevice(device) {
return __awaiter(this, void 0, void 0, function* () {
// Check if already formatted
const result = yield exec.getExecOutput('sudo', ['blkid', device], {
const result = yield getExecOutputAsRoot('blkid', [device], {
ignoreReturnCode: true
});
if (result.exitCode === 0 && result.stdout.includes('TYPE=')) {
core.debug(`Device ${device} is already formatted`);
// Resize to use full block device
try {
yield exec.exec('sudo', ['resize2fs', '-f', device]);
yield execAsRoot('resize2fs', ['-f', device]);
core.debug(`Resized filesystem on ${device}`);
}
catch (_a) {
Expand All @@ -275,8 +295,7 @@ function maybeFormatDevice(device) {
}
// Format with ext4
core.info(`Formatting device ${device} with ext4`);
yield exec.exec('sudo', [
'mkfs.ext4',
yield execAsRoot('mkfs.ext4', [
'-m0',
'-Enodiscard,lazy_itable_init=1,lazy_journal_init=1',
'-F',
Expand Down Expand Up @@ -373,10 +392,10 @@ function setupCache(owner, repo) {
yield maybeFormatDevice(device);
// Mount the device at a unique path for this repository
const mountPoint = getMountPoint(owner, repo);
yield exec.exec('sudo', ['mkdir', '-p', mountPoint]);
yield execAsRoot('mkdir', ['-p', mountPoint]);
// noinit_itable stops the background zeroing of a non-trivial portion of
// the device (uninitialized inode tables), which is unnecessary here.
yield exec.exec('sudo', ['mount', '-o', 'noinit_itable', device, mountPoint]);
yield execAsRoot('mount', ['-o', 'noinit_itable', device, mountPoint]);
core.info(`[git-mirror] Mounted ${device} at ${mountPoint}`);
return {
exposeId,
Expand Down Expand Up @@ -430,12 +449,7 @@ function adoptMirrorOwnership(mirrorPath) {
return;
}
const start = Date.now();
yield exec.exec('sudo', [
'chown',
'-R',
`${uid}:${gid}`,
path.dirname(mirrorPath)
]);
yield execAsRoot('chown', ['-R', `${uid}:${gid}`, path.dirname(mirrorPath)]);
core.info(`[git-mirror] Took over mirror owned by uid ${owner} in ${Date.now() - start}ms`);
});
}
Expand Down Expand Up @@ -531,11 +545,11 @@ function ensureMirror(mirrorPath_1, repoUrl_1, authToken_1) {
const trace2PerfPath = verbose ? newTrace2PerfPath('clone') : undefined;
const gitEnv = buildGitEnv(verbose, trace2PerfPath);
const mirrorDir = path.dirname(mirrorPath);
yield exec.exec('sudo', ['mkdir', '-p', mirrorDir]);
yield execAsRoot('mkdir', ['-p', mirrorDir]);
// Change ownership so git can write to it
const uid = (_b = (_a = process.getuid) === null || _a === void 0 ? void 0 : _a.call(process)) !== null && _b !== void 0 ? _b : 1000;
const gid = (_d = (_c = process.getgid) === null || _c === void 0 ? void 0 : _c.call(process)) !== null && _d !== void 0 ? _d : 1000;
yield exec.exec('sudo', ['chown', '-R', `${uid}:${gid}`, mirrorDir]);
yield execAsRoot('chown', ['-R', `${uid}:${gid}`, mirrorDir]);
yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
// Clean up any partial clone from a previous failed attempt
if (fs.existsSync(mirrorPath)) {
Expand Down Expand Up @@ -1906,10 +1920,7 @@ function flushBlockDevice(devicePath) {
try {
const result = yield exec.getExecOutput('timeout', [
String(FLUSH_TIMEOUT_SECS),
'sudo',
'blockdev',
'--flushbufs',
devicePath
...asRoot('blockdev', ['--flushbufs', devicePath])
], { ignoreReturnCode: true });
const duration = Date.now() - startTime;
if (result.exitCode === TIMEOUT_EXIT_CODE) {
Expand Down Expand Up @@ -2000,7 +2011,7 @@ function cleanup(options) {
for (let attempt = 1; attempt <= UMOUNT_MAX_RETRIES; attempt++) {
core.info(`[git-mirror] Unmounting ${mountPoint} (attempt ${attempt}/${UMOUNT_MAX_RETRIES})`);
try {
const umountResult = yield exec.getExecOutput('timeout', [String(UMOUNT_TIMEOUT_SECS), 'sudo', 'umount', mountPoint], { ignoreReturnCode: true });
const umountResult = yield exec.getExecOutput('timeout', [String(UMOUNT_TIMEOUT_SECS), ...asRoot('umount', [mountPoint])], { ignoreReturnCode: true });
if (umountResult.exitCode === 0) {
unmountSuccess = true;
core.info(`[git-mirror] Successfully unmounted ${mountPoint}`);
Expand Down
57 changes: 35 additions & 22 deletions src/blacksmith-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ const AGENT_RPC_TIMEOUT_MS = 45000
const MOUNT_BASE = '/blacksmith-git-mirror'
const MIRROR_VERSION = 'v1'

/**
* Argv for a command that needs root. Job containers usually run as root
* and often ship without sudo, so the prefix is only added when needed.
*/
function asRoot(cmd: string, args: string[]): string[] {
return process.getuid?.() === 0 ? [cmd, ...args] : ['sudo', cmd, ...args]
}

async function execAsRoot(cmd: string, args: string[]): Promise<number> {
const [file, ...rest] = asRoot(cmd, args)
return exec.exec(file, rest)
}

async function getExecOutputAsRoot(
cmd: string,
args: string[],
options?: exec.ExecOptions
): Promise<exec.ExecOutput> {
const [file, ...rest] = asRoot(cmd, args)
return exec.getExecOutput(file, rest, options)
}

// A sync that doesn't finish within this window is abandoned (single
// attempt, no retries): sync failure is soft - the workspace is populated
// from the mirror's last good state plus a targeted fetch of the job's own
Expand Down Expand Up @@ -237,9 +259,9 @@ async function waitForNonZeroDeviceSize(
): Promise<void> {
const deadline = Date.now() + timeoutMs
for (;;) {
const result = await exec.getExecOutput(
'sudo',
['blockdev', '--getsize64', device],
const result = await getExecOutputAsRoot(
'blockdev',
['--getsize64', device],
{ignoreReturnCode: true, silent: true}
)
if (result.exitCode === 0) {
Expand All @@ -262,15 +284,15 @@ async function waitForNonZeroDeviceSize(
*/
async function maybeFormatDevice(device: string): Promise<void> {
// Check if already formatted
const result = await exec.getExecOutput('sudo', ['blkid', device], {
const result = await getExecOutputAsRoot('blkid', [device], {
ignoreReturnCode: true
})

if (result.exitCode === 0 && result.stdout.includes('TYPE=')) {
core.debug(`Device ${device} is already formatted`)
// Resize to use full block device
try {
await exec.exec('sudo', ['resize2fs', '-f', device])
await execAsRoot('resize2fs', ['-f', device])
core.debug(`Resized filesystem on ${device}`)
} catch {
core.warning(`Error resizing filesystem on ${device}`)
Expand All @@ -280,8 +302,7 @@ async function maybeFormatDevice(device: string): Promise<void> {

// Format with ext4
core.info(`Formatting device ${device} with ext4`)
await exec.exec('sudo', [
'mkfs.ext4',
await execAsRoot('mkfs.ext4', [
'-m0',
'-Enodiscard,lazy_itable_init=1,lazy_journal_init=1',
'-F',
Expand Down Expand Up @@ -403,10 +424,10 @@ export async function setupCache(

// Mount the device at a unique path for this repository
const mountPoint = getMountPoint(owner, repo)
await exec.exec('sudo', ['mkdir', '-p', mountPoint])
await execAsRoot('mkdir', ['-p', mountPoint])
// noinit_itable stops the background zeroing of a non-trivial portion of
// the device (uninitialized inode tables), which is unnecessary here.
await exec.exec('sudo', ['mount', '-o', 'noinit_itable', device, mountPoint])
await execAsRoot('mount', ['-o', 'noinit_itable', device, mountPoint])
core.info(`[git-mirror] Mounted ${device} at ${mountPoint}`)

return {
Expand Down Expand Up @@ -467,12 +488,7 @@ async function adoptMirrorOwnership(mirrorPath: string): Promise<void> {
return
}
const start = Date.now()
await exec.exec('sudo', [
'chown',
'-R',
`${uid}:${gid}`,
path.dirname(mirrorPath)
])
await execAsRoot('chown', ['-R', `${uid}:${gid}`, path.dirname(mirrorPath)])
core.info(
`[git-mirror] Took over mirror owned by uid ${owner} in ${Date.now() - start}ms`
)
Expand Down Expand Up @@ -586,11 +602,11 @@ export async function ensureMirror(
const gitEnv = buildGitEnv(verbose, trace2PerfPath)

const mirrorDir = path.dirname(mirrorPath)
await exec.exec('sudo', ['mkdir', '-p', mirrorDir])
await execAsRoot('mkdir', ['-p', mirrorDir])
// Change ownership so git can write to it
const uid = process.getuid?.() ?? 1000
const gid = process.getgid?.() ?? 1000
await exec.exec('sudo', ['chown', '-R', `${uid}:${gid}`, mirrorDir])
await execAsRoot('chown', ['-R', `${uid}:${gid}`, mirrorDir])
await retryHelper.execute(async () => {
// Clean up any partial clone from a previous failed attempt
if (fs.existsSync(mirrorPath)) {
Expand Down Expand Up @@ -2270,10 +2286,7 @@ async function flushBlockDevice(devicePath: string): Promise<void> {
'timeout',
[
String(FLUSH_TIMEOUT_SECS),
'sudo',
'blockdev',
'--flushbufs',
devicePath
...asRoot('blockdev', ['--flushbufs', devicePath])
],
{ignoreReturnCode: true}
)
Expand Down Expand Up @@ -2421,7 +2434,7 @@ export async function cleanup(options: CleanupOptions): Promise<CleanupResult> {
try {
const umountResult = await exec.getExecOutput(
'timeout',
[String(UMOUNT_TIMEOUT_SECS), 'sudo', 'umount', mountPoint],
[String(UMOUNT_TIMEOUT_SECS), ...asRoot('umount', [mountPoint])],
{ignoreReturnCode: true}
)
if (umountResult.exitCode === 0) {
Expand Down
Loading