mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-19 13:47:52 +00:00
Harden 'remote-build' error handling (balena push)
Change-type: patch Signed-off-by: Paulo Castro <paulo@balena.io>
This commit is contained in:
parent
145b613f5d
commit
1e81638433
@ -194,11 +194,11 @@ export const push: CommandDefinition<
|
|||||||
switch (buildTarget) {
|
switch (buildTarget) {
|
||||||
case BuildTarget.Cloud:
|
case BuildTarget.Cloud:
|
||||||
const app = appOrDevice;
|
const app = appOrDevice;
|
||||||
Bluebird.join(
|
await Bluebird.join(
|
||||||
sdk.auth.getToken(),
|
sdk.auth.getToken(),
|
||||||
sdk.settings.get('balenaUrl'),
|
sdk.settings.get('balenaUrl'),
|
||||||
getAppOwner(sdk, app),
|
getAppOwner(sdk, app),
|
||||||
(token, baseUrl, owner) => {
|
async (token, baseUrl, owner) => {
|
||||||
const opts = {
|
const opts = {
|
||||||
emulated: options.emulated,
|
emulated: options.emulated,
|
||||||
nocache: options.nocache,
|
nocache: options.nocache,
|
||||||
@ -214,14 +214,14 @@ export const push: CommandDefinition<
|
|||||||
opts,
|
opts,
|
||||||
};
|
};
|
||||||
|
|
||||||
return remote.startRemoteBuild(args);
|
return await remote.startRemoteBuild(args);
|
||||||
},
|
},
|
||||||
).nodeify(done);
|
).nodeify(done);
|
||||||
break;
|
break;
|
||||||
case BuildTarget.Device:
|
case BuildTarget.Device:
|
||||||
const device = appOrDevice;
|
const device = appOrDevice;
|
||||||
// TODO: Support passing a different port
|
// TODO: Support passing a different port
|
||||||
Bluebird.resolve(
|
await Bluebird.resolve(
|
||||||
deviceDeploy.deployToDevice({
|
deviceDeploy.deployToDevice({
|
||||||
source,
|
source,
|
||||||
deviceHost: device,
|
deviceHost: device,
|
||||||
|
@ -13,15 +13,17 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import * as Bluebird from 'bluebird';
|
||||||
import * as JSONStream from 'JSONStream';
|
import * as JSONStream from 'JSONStream';
|
||||||
|
import * as readline from 'readline';
|
||||||
import * as request from 'request';
|
import * as request from 'request';
|
||||||
import { BalenaSDK } from 'balena-sdk';
|
|
||||||
import * as Stream from 'stream';
|
import * as Stream from 'stream';
|
||||||
|
import { BalenaSDK } from 'balena-sdk';
|
||||||
import { Pack } from 'tar-stream';
|
import { Pack } from 'tar-stream';
|
||||||
|
import { RegistrySecrets } from 'resin-multibuild';
|
||||||
import { TypedError } from 'typed-error';
|
import { TypedError } from 'typed-error';
|
||||||
|
|
||||||
import { RegistrySecrets } from 'resin-multibuild';
|
import { exitWithExpectedError } from '../utils/patterns';
|
||||||
import { tarDirectory } from './compose';
|
import { tarDirectory } from './compose';
|
||||||
|
|
||||||
const DEBUG_MODE = !!process.env.DEBUG;
|
const DEBUG_MODE = !!process.env.DEBUG;
|
||||||
@ -85,18 +87,16 @@ async function getBuilderEndpoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
|
export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
|
||||||
const Bluebird = await import('bluebird');
|
const stream = await getRemoteBuildStream(build);
|
||||||
|
|
||||||
const stream = await getRequestStream(build);
|
|
||||||
|
|
||||||
// Special windows handling (win64 also reports win32)
|
// Special windows handling (win64 also reports win32)
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const readline = (await import('readline')).createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout,
|
output: process.stdout,
|
||||||
});
|
});
|
||||||
|
|
||||||
readline.on('SIGINT', () => process.emit('SIGINT'));
|
rl.on('SIGINT', () => process.emit('SIGINT'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Bluebird((resolve, reject) => {
|
return new Bluebird((resolve, reject) => {
|
||||||
@ -126,13 +126,11 @@ export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBuilderMetadata(obj: BuilderMessage, build: RemoteBuild) {
|
function handleBuilderMetadata(obj: BuilderMessage, build: RemoteBuild) {
|
||||||
const { stripIndent } = await import('common-tags');
|
const { stripIndent } = require('common-tags');
|
||||||
|
|
||||||
switch (obj.resource) {
|
switch (obj.resource) {
|
||||||
case 'cursor':
|
case 'cursor':
|
||||||
const readline = await import('readline');
|
|
||||||
|
|
||||||
if (obj.value == null) {
|
if (obj.value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -177,8 +175,8 @@ async function handleBuilderMetadata(obj: BuilderMessage, build: RemoteBuild) {
|
|||||||
|
|
||||||
function getBuilderMessageHandler(
|
function getBuilderMessageHandler(
|
||||||
build: RemoteBuild,
|
build: RemoteBuild,
|
||||||
): (obj: BuilderMessage) => Promise<void> {
|
): (obj: BuilderMessage) => void {
|
||||||
return async (obj: BuilderMessage) => {
|
return (obj: BuilderMessage) => {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
console.log(`[debug] handling message: ${JSON.stringify(obj)}`);
|
console.log(`[debug] handling message: ${JSON.stringify(obj)}`);
|
||||||
}
|
}
|
||||||
@ -186,7 +184,6 @@ function getBuilderMessageHandler(
|
|||||||
return handleBuilderMetadata(obj, build);
|
return handleBuilderMetadata(obj, build);
|
||||||
}
|
}
|
||||||
if (obj.message) {
|
if (obj.message) {
|
||||||
const readline = await import('readline');
|
|
||||||
readline.clearLine(process.stdout, 0);
|
readline.clearLine(process.stdout, 0);
|
||||||
|
|
||||||
const message = obj.message.replace(TRIM_REGEX, '');
|
const message = obj.message.replace(TRIM_REGEX, '');
|
||||||
@ -216,72 +213,119 @@ async function cancelBuildIfNecessary(build: RemoteBuild): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a callback function that takes a tar-stream Pack object as argument
|
* Call tarDirectory() with a suitable callback to insert registry secrets in
|
||||||
* and uses it to add the '.balena/registry-secrets.json' metadata file that
|
* the tar stream, and return the stream.
|
||||||
* contains usernames and passwords to private docker registries. The builder
|
|
||||||
* will remove the file from the tar stream and use the secrets to pull base
|
|
||||||
* images from users' private registries.
|
|
||||||
* @param registrySecrets JS object containing registry usernames and passwords
|
|
||||||
* @returns A callback function, or undefined if registrySecrets is empty
|
|
||||||
*/
|
*/
|
||||||
function getTarStreamCallbackForRegistrySecrets(
|
async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
|
||||||
registrySecrets: RegistrySecrets,
|
const path = await import('path');
|
||||||
): ((pack: Pack) => void) | undefined {
|
const visuals = await import('resin-cli-visuals');
|
||||||
if (Object.keys(registrySecrets).length > 0) {
|
const tarSpinner = new visuals.Spinner('Packaging the project source...');
|
||||||
return (pack: Pack) => {
|
const preFinalizeCallback = (pack: Pack) => {
|
||||||
pack.entry(
|
pack.entry(
|
||||||
{ name: '.balena/registry-secrets.json' },
|
{ name: '.balena/registry-secrets.json' },
|
||||||
JSON.stringify(registrySecrets),
|
JSON.stringify(build.opts.registrySecrets),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
tarSpinner.start();
|
||||||
|
return await tarDirectory(
|
||||||
|
path.resolve(build.source),
|
||||||
|
Object.keys(build.opts.registrySecrets).length > 0
|
||||||
|
? preFinalizeCallback
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
tarSpinner.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRequestStream(build: RemoteBuild): Promise<Stream.Duplex> {
|
/**
|
||||||
const path = await import('path');
|
* Initiate a POST HTTP request to the remote builder and add some event
|
||||||
const visuals = await import('resin-cli-visuals');
|
* listeners.
|
||||||
const zlib = await import('zlib');
|
*
|
||||||
|
* ¡! Note: this function must be synchronous because of a bug in the `request`
|
||||||
const tarSpinner = new visuals.Spinner('Packaging the project source...');
|
* library that requires the following two steps to take place in the same
|
||||||
tarSpinner.start();
|
* iteration of Node's event loop: (1) adding a listener for the 'response'
|
||||||
// Tar the directory so that we can send it to the builder
|
* event and (2) calling request.pipe():
|
||||||
const tarStream = await tarDirectory(
|
* https://github.com/request/request/issues/887
|
||||||
path.resolve(build.source),
|
*/
|
||||||
getTarStreamCallbackForRegistrySecrets(build.opts.registrySecrets),
|
function createRemoteBuildRequest(
|
||||||
|
build: RemoteBuild,
|
||||||
|
tarStream: Stream.Readable,
|
||||||
|
builderUrl: string,
|
||||||
|
onError: (error: Error) => void,
|
||||||
|
): request.Request {
|
||||||
|
const zlib = require('zlib');
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
console.log(`[debug] Connecting to builder at ${builderUrl}`);
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
.post({
|
||||||
|
url: builderUrl,
|
||||||
|
auth: { bearer: build.auth },
|
||||||
|
headers: { 'Content-Encoding': 'gzip' },
|
||||||
|
body: tarStream.pipe(zlib.createGzip({ level: 6 })),
|
||||||
|
})
|
||||||
|
.on('error', onError)
|
||||||
|
.once('response', (response: request.RequestResponse) => {
|
||||||
|
if (response.statusCode >= 100 && response.statusCode < 400) {
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
console.log(
|
||||||
|
`[debug] received HTTP ${response.statusCode} ${
|
||||||
|
response.statusMessage
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
tarSpinner.stop();
|
}
|
||||||
|
} else {
|
||||||
|
let msgArr = [
|
||||||
|
'Remote builder responded with HTTP error:',
|
||||||
|
`${response.statusCode} ${response.statusMessage}`,
|
||||||
|
];
|
||||||
|
if (response.body) {
|
||||||
|
msgArr.push(response.body);
|
||||||
|
}
|
||||||
|
onError(new Error(msgArr.join('\n')));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const url = await getBuilderEndpoint(
|
async function getRemoteBuildStream(
|
||||||
|
build: RemoteBuild,
|
||||||
|
): Promise<NodeJS.ReadWriteStream> {
|
||||||
|
const tarStream = await getTarStream(build);
|
||||||
|
const visuals = await import('resin-cli-visuals');
|
||||||
|
const uploadSpinner = new visuals.Spinner(
|
||||||
|
'Uploading source package to balena cloud',
|
||||||
|
);
|
||||||
|
const exitOnError = (error: Error): never => {
|
||||||
|
uploadSpinner.stop();
|
||||||
|
return exitWithExpectedError(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
uploadSpinner.start();
|
||||||
|
const builderUrl = await getBuilderEndpoint(
|
||||||
build.baseUrl,
|
build.baseUrl,
|
||||||
build.owner,
|
build.owner,
|
||||||
build.app,
|
build.app,
|
||||||
build.opts,
|
build.opts,
|
||||||
);
|
);
|
||||||
|
const buildRequest = createRemoteBuildRequest(
|
||||||
if (DEBUG_MODE) {
|
build,
|
||||||
console.log(`[debug] Connecting to builder at ${url}`);
|
tarStream,
|
||||||
}
|
builderUrl,
|
||||||
const post = request.post({
|
exitOnError,
|
||||||
url,
|
|
||||||
auth: {
|
|
||||||
bearer: build.auth,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
'Content-Encoding': 'gzip',
|
|
||||||
},
|
|
||||||
body: tarStream.pipe(
|
|
||||||
zlib.createGzip({
|
|
||||||
level: 6,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadSpinner = new visuals.Spinner(
|
|
||||||
'Uploading source package to balena cloud',
|
|
||||||
);
|
);
|
||||||
uploadSpinner.start();
|
return buildRequest.pipe(
|
||||||
|
JSONStream.parse('*')
|
||||||
const parseStream = post.pipe(JSONStream.parse('*'));
|
.once('close', () => uploadSpinner.stop())
|
||||||
parseStream.on('data', () => uploadSpinner.stop());
|
.once('data', () => uploadSpinner.stop())
|
||||||
return parseStream as Stream.Duplex;
|
.once('end', () => uploadSpinner.stop())
|
||||||
|
.once('error', () => uploadSpinner.stop())
|
||||||
|
.once('finish', () => uploadSpinner.stop()),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return exitOnError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user