WIP remove request library

Change-type: patch
This commit is contained in:
Otavio Jacobi 2024-07-18 10:27:29 -03:00
parent 544f615ca0
commit a1d691da02
7 changed files with 126 additions and 148 deletions

View File

@ -19,7 +19,7 @@ import { getVisuals } from './lazy';
import { promisify } from 'util';
import type * as Dockerode from 'dockerode';
import type Logger = require('./logger');
import type { Request } from 'request';
import type got from 'got';
const getBuilderPushEndpoint = function (
baseUrl: string,
@ -75,7 +75,10 @@ const showPushProgress = function (message: string) {
return progressBar;
};
const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
const uploadToPromise = (
uploadRequest: ReturnType<typeof got.stream.post>,
logger: Logger,
) =>
new Promise<{ buildId: number }>(function (resolve, reject) {
uploadRequest.on('error', reject).on('data', function handleMessage(data) {
let obj;
@ -109,7 +112,7 @@ const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
/**
* @returns {Promise<{ buildId: number }>}
*/
const uploadImage = function (
const uploadImage = async function (
imageStream: NodeJS.ReadableStream & { length: number },
token: string,
username: string,
@ -117,7 +120,7 @@ const uploadImage = function (
appName: string,
logger: Logger,
): Promise<{ buildId: number }> {
const request = require('request') as typeof import('request');
const { default: got } = require('got') as typeof import('got');
const progressStream =
require('progress-stream') as typeof import('progress-stream');
const zlib = require('zlib') as typeof import('zlib');
@ -141,25 +144,22 @@ const uploadImage = function (
),
);
const uploadRequest = request.post({
url: getBuilderPushEndpoint(url, username, appName),
headers: {
'Content-Encoding': 'gzip',
const uploadRequest = got.stream.post(
getBuilderPushEndpoint(url, username, appName),
{
headers: {
'Content-Encoding': 'gzip',
Authorization: `Bearer ${token}`,
},
body: streamWithProgress.pipe(zlib.createGzip({ level: 6 })),
throwHttpErrors: false,
},
auth: {
bearer: token,
},
body: streamWithProgress.pipe(
zlib.createGzip({
level: 6,
}),
),
});
);
return uploadToPromise(uploadRequest, logger);
};
const uploadLogs = function (
const uploadLogs = async function (
logs: string,
token: string,
url: string,
@ -167,15 +167,18 @@ const uploadLogs = function (
username: string,
appName: string,
) {
const request = require('request') as typeof import('request');
return request.post({
json: true,
url: getBuilderLogPushEndpoint(url, buildId, username, appName),
auth: {
bearer: token,
const { default: got } = await import('got');
return await got.post(
getBuilderLogPushEndpoint(url, buildId, username, appName),
{
body: Buffer.from(logs),
headers: {
Authorization: `Bearer ${token}`,
},
responseType: 'json',
throwHttpErrors: false,
},
body: Buffer.from(logs),
});
);
};
/**

View File

@ -15,12 +15,13 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import * as request from 'request';
import type * as Stream from 'stream';
import { retry } from '../helpers';
import Logger = require('../logger');
import * as ApiErrors from './errors';
import { getBalenaSdk } from '../lazy';
import type { BalenaSDK } from 'balena-sdk';
export interface DeviceResponse {
[key: string]: any;
@ -82,7 +83,7 @@ export class DeviceAPI {
// Either return nothing, or throw an error with the info
public async setTargetState(state: any): Promise<void> {
const url = this.getUrlForAction('setTargetState');
return DeviceAPI.promisifiedRequest(
return await DeviceAPI.sendRequest(
{
method: 'POST',
url,
@ -96,7 +97,7 @@ export class DeviceAPI {
public async getTargetState(): Promise<any> {
const url = this.getUrlForAction('getTargetState');
return DeviceAPI.promisifiedRequest(
return await DeviceAPI.sendRequest(
{
method: 'GET',
url,
@ -111,7 +112,7 @@ export class DeviceAPI {
public async getDeviceInformation(): Promise<DeviceInfo> {
const url = this.getUrlForAction('getDeviceInformation');
return DeviceAPI.promisifiedRequest(
return await DeviceAPI.sendRequest(
{
method: 'GET',
url,
@ -126,7 +127,7 @@ export class DeviceAPI {
public async getContainerId(serviceName: string): Promise<string> {
const url = this.getUrlForAction('containerId');
const body = await DeviceAPI.promisifiedRequest(
const body = await DeviceAPI.sendRequest(
{
method: 'GET',
url,
@ -149,7 +150,7 @@ export class DeviceAPI {
public async ping(): Promise<void> {
const url = this.getUrlForAction('ping');
return DeviceAPI.promisifiedRequest(
return await DeviceAPI.sendRequest(
{
method: 'GET',
url,
@ -158,10 +159,10 @@ export class DeviceAPI {
);
}
public getVersion(): Promise<string> {
public async getVersion(): Promise<string> {
const url = this.getUrlForAction('version');
return DeviceAPI.promisifiedRequest({
return await DeviceAPI.sendRequest({
method: 'GET',
url,
json: true,
@ -176,10 +177,10 @@ export class DeviceAPI {
});
}
public getStatus(): Promise<Status> {
public async getStatus(): Promise<Status> {
const url = this.getUrlForAction('status');
return DeviceAPI.promisifiedRequest({
return await DeviceAPI.sendRequest({
method: 'GET',
url,
json: true,
@ -194,30 +195,33 @@ export class DeviceAPI {
});
}
public getLogStream(): Promise<Stream.Readable> {
public async getLogStream(): Promise<Stream.Readable> {
const url = this.getUrlForAction('logs');
const sdk = getBalenaSdk();
return sdk.request.stream({ url });
// Don't use the promisified version here as we want to stream the output
return new Promise((resolve, reject) => {
const req = request.get(url);
// return new Promise((resolve, reject) => {
// const stream = got.stream.get(url, { throwHttpErrors: false });
req.on('error', reject).on('response', async (res) => {
if (res.statusCode !== 200) {
reject(
new ApiErrors.DeviceAPIError(
'Non-200 response from log streaming endpoint',
),
);
return;
}
try {
res.socket.setKeepAlive(true, 1000);
} catch (error) {
reject(error);
}
resolve(res);
});
});
// // stream
// // .on('data', async () => {
// // // if (res.statusCode !== 200) {
// // // reject(
// // // new ApiErrors.DeviceAPIError(
// // // 'Non-200 response from log streaming endpoint',
// // // ),
// // // );
// // // return;
// // // }
// // // try {
// // // stream.socket.setKeepAlive(true, 1000);
// // // } catch (error) {
// // // reject(error);
// // // }
// // });
// resolve(stream);
// });
}
private getUrlForAction(action: keyof typeof deviceEndpoints): string {
@ -226,50 +230,33 @@ export class DeviceAPI {
// A helper method for promisifying general (non-streaming) requests. Streaming
// requests should use a seperate setup
private static async promisifiedRequest<
T extends Parameters<typeof request>[0],
>(opts: T, logger?: Logger): Promise<any> {
interface ObjectWithUrl {
url?: string;
private static async sendRequest(
opts: Parameters<BalenaSDK['request']['send']>[number],
logger?: Logger,
): Promise<any> {
if (logger != null && opts.url != null) {
logger.logDebug(`Sending request to ${opts.url}`);
}
if (logger != null) {
let url: string | null = null;
if (_.isObject(opts) && (opts as ObjectWithUrl).url != null) {
// the `as string` shouldn't be necessary, but the type system
// is getting a little confused
url = (opts as ObjectWithUrl).url as string;
} else if (typeof opts === 'string') {
url = opts;
}
if (url != null) {
logger.logDebug(`Sending request to ${url}`);
}
}
const sdk = getBalenaSdk();
const doRequest = async () => {
return await new Promise((resolve, reject) => {
return request(opts, (err, response, body) => {
if (err) {
return reject(err);
}
switch (response.statusCode) {
case 200:
return resolve(body);
case 400:
return reject(
new ApiErrors.BadRequestDeviceAPIError(body.message),
);
case 503:
return reject(
new ApiErrors.ServiceUnavailableAPIError(body.message),
);
default:
return reject(new ApiErrors.DeviceAPIError(body.message));
}
});
});
const response = await sdk.request.send(opts);
const bodyError =
typeof response.body === 'string'
? response.body
: response.body.message;
switch (response.statusCode) {
case 200:
return response.body;
case 400:
throw new ApiErrors.BadRequestDeviceAPIError(bodyError);
case 503:
throw new ApiErrors.ServiceUnavailableAPIError(bodyError);
default:
new ApiErrors.DeviceAPIError(bodyError);
}
};
return await retry({

View File

@ -94,7 +94,7 @@ async function installQemu(arch: string, qemuPath: string) {
const urlVersion = encodeURIComponent(QEMU_VERSION);
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`;
const request = await import('request');
const { default: got } = await import('got');
const fs = await import('fs');
const zlib = await import('zlib');
const tar = await import('tar-stream');
@ -117,7 +117,8 @@ async function installQemu(arch: string, qemuPath: string) {
reject(err);
}
});
request(qemuUrl)
got.stream
.get(qemuUrl, { throwHttpErrors: false })
.on('error', reject)
.pipe(zlib.createGunzip())
.on('error', reject)

View File

@ -16,7 +16,7 @@ limitations under the License.
import type { BalenaSDK } from 'balena-sdk';
import * as JSONStream from 'JSONStream';
import * as readline from 'readline';
import * as request from 'request';
import got from 'got';
import type { RegistrySecrets } from '@balena/compose/dist/multibuild';
import type * as Stream from 'stream';
import streamToPromise = require('stream-to-promise');
@ -27,6 +27,8 @@ import { tarDirectory } from './compose_ts';
import { getVisuals, stripIndent } from './lazy';
import Logger = require('./logger');
type GotStreamRequest = ReturnType<typeof got.stream.post>;
const globalLogger = Logger.getLogger();
const DEBUG_MODE = !!process.env.DEBUG;
@ -119,7 +121,7 @@ export async function startRemoteBuild(
} catch (err) {
console.error(err.message);
} finally {
buildRequest.abort();
buildRequest.destroy();
const sigintErr = new SIGINTError('Build aborted on SIGINT signal');
sigintErr.code = 'SIGINT';
stream.emit('error', sigintErr);
@ -336,32 +338,28 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
/**
* Initiate a POST HTTP request to the remote builder and add some event
* listeners.
*
* ¡! Note: this function must be synchronous because of a bug in the `request`
* library that requires the following two steps to take place in the same
* iteration of Node's event loop: (1) adding a listener for the 'response'
* event and (2) calling request.pipe():
* https://github.com/request/request/issues/887
*/
function createRemoteBuildRequest(
build: RemoteBuild,
tarStream: Stream.Readable,
builderUrl: string,
onError: (error: Error) => void,
): request.Request {
) {
const zlib = require('zlib') as typeof import('zlib');
if (DEBUG_MODE) {
console.error(`[debug] Connecting to builder at ${builderUrl}`);
}
return request
.post({
url: builderUrl,
auth: { bearer: build.auth },
headers: { 'Content-Encoding': 'gzip' },
return got.stream
.post(builderUrl, {
headers: {
'Content-Encoding': 'gzip',
Authorization: `Bearer ${build.auth}`,
},
body: tarStream.pipe(zlib.createGzip({ level: 6 })),
throwHttpErrors: false,
})
.once('error', onError) // `.once` because the handler re-emits
.once('response', (response: request.RequestResponse) => {
.once('response', (response) => {
if (response.statusCode >= 100 && response.statusCode < 400) {
if (DEBUG_MODE) {
console.error(
@ -383,7 +381,7 @@ function createRemoteBuildRequest(
async function getRemoteBuildStream(
build: RemoteBuild,
): Promise<[request.Request, Stream.Stream]> {
): Promise<[GotStreamRequest, Stream.Stream]> {
const builderUrl = await getBuilderEndpoint(
build.baseUrl,
build.appSlug,

14
npm-shrinkwrap.json generated
View File

@ -68,7 +68,6 @@
"prettyjson": "^1.2.5",
"progress-stream": "^2.0.0",
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
"request": "^2.88.2",
"resin-cli-form": "^3.0.0",
"resin-cli-visuals": "^2.0.0",
"resin-doodles": "^0.2.0",
@ -11120,11 +11119,10 @@
}
},
"node_modules/is-core-module": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz",
"integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==",
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
@ -16391,9 +16389,9 @@
}
},
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"bin": {
"semver": "bin/semver.js"
},

View File

@ -257,7 +257,6 @@
"prettyjson": "^1.2.5",
"progress-stream": "^2.0.0",
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
"request": "^2.88.2",
"resin-cli-form": "^3.0.0",
"resin-cli-visuals": "^2.0.0",
"resin-doodles": "^0.2.0",

View File

@ -16,11 +16,11 @@
*/
import * as chai from 'chai';
import chaiAsPromised = require('chai-as-promised');
import * as chaiAsPromised from 'chai-as-promised';
import * as ejs from 'ejs';
import * as fs from 'fs';
import * as path from 'path';
import * as request from 'request';
import got from 'got';
import * as sinon from 'sinon';
import { LoginServer } from '../../build/auth/server';
@ -67,32 +67,24 @@ describe('Login server:', function () {
expectedStatusCode: number;
expectedToken: string;
urlPath?: string;
verb?: string;
verb?: 'get' | 'post';
}) {
opt.urlPath = opt.urlPath ?? addr.urlPath;
const post = opt.verb
? ((request as any)[opt.verb] as typeof request.post)
: request.post;
await new Promise<void>((resolve, reject) => {
post(
`http://${addr.host}:${addr.port}${opt.urlPath}`,
{
form: {
token: opt.expectedToken,
},
const request = opt.verb != null ? got[opt.verb] : got.post;
const res = await request(
`http://${addr.host}:${addr.port}${opt.urlPath}`,
{
form: {
token: opt.expectedToken,
},
function (error, response, body) {
try {
expect(error).to.not.exist;
expect(response.statusCode).to.equal(opt.expectedStatusCode);
expect(body).to.equal(opt.expectedBody);
resolve();
} catch (err) {
reject(err);
}
},
);
});
throwHttpErrors: false,
// This ensures we can test the expected response in case we do that (a 404)
allowGetBody: true,
},
);
expect(res.body).to.equal(opt.expectedBody);
expect(res.statusCode).to.equal(opt.expectedStatusCode);
try {
const token = await server.awaitForToken();