build, deploy: Fix error handling when QEMU download fails

Change-type: patch
This commit is contained in:
Paulo Castro 2021-03-06 15:06:17 +00:00
parent 8b99cd7170
commit bcea5193a1
2 changed files with 64 additions and 39 deletions

View File

@ -1,6 +1,6 @@
/** /**
* @license * @license
* Copyright 2017-2020 Balena Ltd. * Copyright 2017-2021 Balena Ltd.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,8 @@
*/ */
import type * as Dockerode from 'dockerode'; import type * as Dockerode from 'dockerode';
import { ExpectedError } from '../errors';
import { getBalenaSdk, stripIndent } from './lazy'; import { getBalenaSdk, stripIndent } from './lazy';
import Logger = require('./logger'); import Logger = require('./logger');
@ -63,7 +65,8 @@ export function copyQemu(context: string, arch: string) {
.then(() => path.relative(context, binPath)); .then(() => path.relative(context, binPath));
} }
export const getQemuPath = function (arch: string) { export const getQemuPath = function (balenaArch: string) {
const qemuArch = balenaArchToQemuArch(balenaArch);
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const path = require('path') as typeof import('path'); const path = require('path') as typeof import('path');
const { promises: fs } = require('fs') as typeof import('fs'); const { promises: fs } = require('fs') as typeof import('fs');
@ -79,64 +82,76 @@ export const getQemuPath = function (arch: string) {
throw err; throw err;
}) })
.then(() => .then(() =>
path.join(binDir, `${QEMU_BIN_NAME}-${arch}-${QEMU_VERSION}`), path.join(binDir, `${QEMU_BIN_NAME}-${qemuArch}-${QEMU_VERSION}`),
), ),
); );
}; };
export function installQemu(arch: string) { async function installQemu(arch: string, qemuPath: string) {
const request = require('request') as typeof import('request'); const qemuArch = balenaArchToQemuArch(arch);
const fs = require('fs') as typeof import('fs'); const fileVersion = QEMU_VERSION.replace('v', '').replace('+', '.');
const zlib = require('zlib') as typeof import('zlib'); const urlFile = encodeURIComponent(`qemu-${fileVersion}-${qemuArch}.tar.gz`);
const tar = require('tar-stream') as typeof import('tar-stream'); const urlVersion = encodeURIComponent(QEMU_VERSION);
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`;
return getQemuPath(arch).then( const request = await import('request');
(qemuPath) => const fs = await import('fs');
new Promise(function (resolve, reject) { const zlib = await import('zlib');
const installStream = fs.createWriteStream(qemuPath); const tar = await import('tar-stream');
const qemuArch = balenaArchToQemuArch(arch); // createWriteStream creates a zero-length file on disk that
const fileVersion = QEMU_VERSION.replace('v', '').replace('+', '.'); // needs to be deleted if the download fails
const urlFile = encodeURIComponent( const installStream = fs.createWriteStream(qemuPath);
`qemu-${fileVersion}-${qemuArch}.tar.gz`, try {
); await new Promise<void>((resolve, reject) => {
const urlVersion = encodeURIComponent(QEMU_VERSION); const extract = tar.extract();
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`; extract.on('entry', function (header, stream, next) {
try {
const extract = tar.extract();
extract.on('entry', function (header, stream, next) {
stream.on('end', next); stream.on('end', next);
if (header.name.includes(`qemu-${qemuArch}-static`)) { if (header.name.includes(`qemu-${qemuArch}-static`)) {
stream.pipe(installStream); stream.pipe(installStream);
} else { } else {
stream.resume(); stream.resume();
} }
} catch (err) {
reject(err);
}
});
request(qemuUrl)
.on('error', reject)
.pipe(zlib.createGunzip())
.on('error', reject)
.pipe(extract)
.on('error', reject)
.on('finish', function () {
fs.chmodSync(qemuPath, '755');
resolve();
}); });
});
return request(qemuUrl) } catch (err) {
.on('error', reject) try {
.pipe(zlib.createGunzip()) await fs.promises.unlink(qemuPath);
.on('error', reject) } catch {
.pipe(extract) // ignore
.on('error', reject) }
.on('finish', function () { throw err;
fs.chmodSync(qemuPath, '755'); }
resolve();
});
}),
);
} }
const balenaArchToQemuArch = function (arch: string) { const balenaArchToQemuArch = function (arch: string) {
switch (arch) { switch (arch) {
case 'armv7hf':
case 'rpi': case 'rpi':
case 'arm':
case 'armhf': case 'armhf':
case 'armv7hf':
return 'arm'; return 'arm';
case 'arm64':
case 'aarch64': case 'aarch64':
return 'aarch64'; return 'aarch64';
default: default:
throw new Error(`Cannot install emulator for architecture ${arch}`); throw new ExpectedError(stripIndent`
Unknown ARM architecture identifier "${arch}".
Known ARM identifiers: rpi arm armhf armv7hf arm64 aarch64`);
} }
}; };
@ -155,11 +170,17 @@ export async function installQemuIfNeeded(
const { promises: fs } = await import('fs'); const { promises: fs } = await import('fs');
const qemuPath = await getQemuPath(arch); const qemuPath = await getQemuPath(arch);
try { try {
const stats = await fs.stat(qemuPath);
// Earlier versions of the CLI with broken error handling would leave
// behind files with size 0. If such a file is found, delete it.
if (stats.size === 0) {
await fs.unlink(qemuPath);
}
await fs.access(qemuPath); await fs.access(qemuPath);
} catch { } catch {
// Qemu doesn't exist so install it // QEMU not found in cache folder (~/.balena/bin/), so install it
logger.logInfo(`Installing qemu for ${arch} emulation...`); logger.logInfo(`Installing qemu for ${arch} emulation...`);
await installQemu(arch); await installQemu(arch, qemuPath);
} }
return true; return true;
} }

View File

@ -257,12 +257,16 @@ describe('balena build', function () {
const qemuMod = require(qemuModPath); const qemuMod = require(qemuModPath);
const qemuBinPath = await qemuMod.getQemuPath(arch); const qemuBinPath = await qemuMod.getQemuPath(arch);
try { try {
// patch fs.access and fs.stat to pretend that a copy of the Qemu binary
// already exists locally, thus preventing a download during tests
mock(fsModPath, { mock(fsModPath, {
...fsMod, ...fsMod,
promises: { promises: {
...fsMod.promises, ...fsMod.promises,
access: async (p: string) => access: async (p: string) =>
p === qemuBinPath ? undefined : fsMod.promises.access(p), p === qemuBinPath ? undefined : fsMod.promises.access(p),
stat: async (p: string) =>
p === qemuBinPath ? { size: 1 } : fsMod.promises.stat(p),
}, },
}); });
mock(qemuModPath, { mock(qemuModPath, {