diff --git a/lib/utils/deploy-legacy.ts b/lib/utils/deploy-legacy.ts index 074e9cf2..9096fb2f 100644 --- a/lib/utils/deploy-legacy.ts +++ b/lib/utils/deploy-legacy.ts @@ -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, + 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), - }); + ); }; /** diff --git a/lib/utils/device/api.ts b/lib/utils/device/api.ts index 7687ed25..36bca6de 100644 --- a/lib/utils/device/api.ts +++ b/lib/utils/device/api.ts @@ -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 { 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 { 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 { 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 { 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 { 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 { + public async getVersion(): Promise { 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 { + public async getStatus(): Promise { 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 { + public async getLogStream(): Promise { 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[0], - >(opts: T, logger?: Logger): Promise { - interface ObjectWithUrl { - url?: string; + private static async sendRequest( + opts: Parameters[number], + logger?: Logger, + ): Promise { + 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({ diff --git a/lib/utils/qemu.ts b/lib/utils/qemu.ts index a9e379dc..04d53681 100644 --- a/lib/utils/qemu.ts +++ b/lib/utils/qemu.ts @@ -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) diff --git a/lib/utils/remote-build.ts b/lib/utils/remote-build.ts index 353b653d..1bb65e8d 100644 --- a/lib/utils/remote-build.ts +++ b/lib/utils/remote-build.ts @@ -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; + 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 { /** * 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, diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9108596a..4e05977c 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -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" }, diff --git a/package.json b/package.json index c232886c..2d649b83 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/auth/server.spec.ts b/tests/auth/server.spec.ts index 6a399d1b..8e2f36ec 100644 --- a/tests/auth/server.spec.ts +++ b/tests/auth/server.spec.ts @@ -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((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();