mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 10:35:39 +00:00
Compare commits
26 Commits
remove-req
...
master
Author | SHA1 | Date | |
---|---|---|---|
3f9288e9d3 | |||
0efa745628 | |||
bddad252f7 | |||
a1a0e4f028 | |||
de74baa2ff | |||
6c12f755c5 | |||
f80b8e63b1 | |||
b32514f5af | |||
935f8d2549 | |||
b2de857ef1 | |||
78f1471bf4 | |||
d47abf072d | |||
8502c4db4b | |||
dd2c5c40d7 | |||
d23b253ac5 | |||
0b0e24c9b2 | |||
f9656cbe91 | |||
e174f7db4c | |||
a7a408a5c7 | |||
e5877c7de9 | |||
ecb8b3ae6b | |||
fd20516f69 | |||
5ccee0e4f1 | |||
7bb13a551c | |||
19d287aefc | |||
8d10c1af2a |
2
.github/actions/publish/action.yml
vendored
2
.github/actions/publish/action.yml
vendored
@ -18,7 +18,7 @@ inputs:
|
||||
default: 'accounts+apple@balena.io'
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '20.x'
|
||||
default: '22.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: 'true'
|
||||
|
2
.github/actions/test/action.yml
vendored
2
.github/actions/test/action.yml
vendored
@ -15,7 +15,7 @@ inputs:
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '20.x'
|
||||
default: '22.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
@ -1,3 +1,192 @@
|
||||
- commits:
|
||||
- subject: "Deploy: Limit the submitted error_message of images that fail to build
|
||||
to 1000 characters"
|
||||
hash: bddad252f7cb412a3d417be1d7bd7e4ed9726b8e
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
See: https://balena.fibery.io/Work/Project/re-pitching-API-Limit-size-of-large-fields-975
|
||||
see: https://balena.fibery.io/Work/Project/re-pitching-API-Limit-size-of-large-fields-975
|
||||
author: Thodoris Greasidis
|
||||
nested: []
|
||||
version: 22.1.1
|
||||
title: ""
|
||||
date: 2025-06-19T09:32:52.976Z
|
||||
- commits:
|
||||
- subject: Add support for node 22
|
||||
hash: f80b8e63b1e5b22dd95b034fe14da0a7e3ab4986
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
author: Otavio Jacobi
|
||||
nested: []
|
||||
- subject: Bump etcher-sdk to v10.0.0
|
||||
hash: b32514f5afb4fdd12e4a9dca5e2a1d53e727b434
|
||||
body: |
|
||||
Update balena-device-init from 8.1.3 to 8.1.1
|
||||
Update etcher-sdk from 9.1.4 to 10.0.0
|
||||
Update resin-cli-form from 3.0.0 to 4.0.0
|
||||
Update resin-cli-visuals from 2.0.1 to 3.0.0
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Otavio Jacobi
|
||||
nested:
|
||||
- commits:
|
||||
- subject: Drop support to node18 and add support to node 22 & 24
|
||||
hash: 95e577823f642a6c0e500aa29fc150b7807d84f7
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: major
|
||||
change-type: major
|
||||
author: Otavio Jacobi
|
||||
nested: []
|
||||
version: etcher-sdk-10.0.0
|
||||
title: ""
|
||||
date: 2025-06-02T09:12:32.868Z
|
||||
version: 22.1.0
|
||||
title: ""
|
||||
date: 2025-06-09T20:05:08.020Z
|
||||
- commits:
|
||||
- subject: Remove `request` dependency
|
||||
hash: d47abf072ddf1f6529f3d4a14e07436def58df61
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
- subject: Replace `request` usage with `got`
|
||||
hash: 8502c4db4bb211a70c682dbc1b85df56f01f2d93
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
version: 22.0.6
|
||||
title: ""
|
||||
date: 2025-06-02T12:27:11.068Z
|
||||
- commits:
|
||||
- subject: Bump etcher-sdk to v9.1.4
|
||||
hash: 0b0e24c9b29ef4bcb6a577ca85708171cc2421c7
|
||||
body: |
|
||||
Update etcher-sdk from 9.1.0 to 9.1.4
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Otavio Jacobi
|
||||
nested:
|
||||
- commits:
|
||||
- subject: Run `npm audit fix` which should only do non-breaking changes
|
||||
hash: 22aaacc0744e41989706c968c4efc8767d30b7a8
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Otavio Jacobi
|
||||
nested: []
|
||||
version: etcher-sdk-9.1.4
|
||||
title: ""
|
||||
date: 2025-05-29T08:57:28.785Z
|
||||
- commits:
|
||||
- subject: Embed config.json with a fixed timestamp to enable consistent checksums
|
||||
hash: 83e67a4089ec39023c39fe79fe59021237797c85
|
||||
body: >
|
||||
Previously the timestamp was changing each time which meant the
|
||||
|
||||
checksum would be different every time even if the rest of the
|
||||
contents
|
||||
|
||||
were identical. Changing this to a fixed timestamp avoids that
|
||||
change
|
||||
|
||||
such that only the contents matter.
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Pagan Gazzard
|
||||
nested: []
|
||||
version: etcher-sdk-9.1.3
|
||||
title: ""
|
||||
date: 2025-02-17T12:48:33.911Z
|
||||
- commits:
|
||||
- subject: Update dependency unzip-stream to v0.3.2 [SECURITY]
|
||||
hash: c243e56e4189bee7391943a3325a3c1465c62fd1
|
||||
body: |
|
||||
Update unzip-stream from 0.3.1 to 0.3.2
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Self-hosted Renovate Bot
|
||||
nested: []
|
||||
version: etcher-sdk-9.1.2
|
||||
title: ""
|
||||
date: 2024-10-09T08:52:13.524Z
|
||||
- commits:
|
||||
- subject: "patch: add EXLOCK flag for windows"
|
||||
hash: 915feeeceff83249f87a6a0a1656986791206136
|
||||
body: |
|
||||
Signed-off-by: Talha Can Havadar <havadartalha@gmail.com>
|
||||
|
||||
run prettier for changed files
|
||||
footer:
|
||||
Signed-off-by: Talha Can Havadar <havadartalha@gmail.com>
|
||||
signed-off-by: Talha Can Havadar <havadartalha@gmail.com>
|
||||
author: Talha Can Havadar
|
||||
nested: []
|
||||
version: etcher-sdk-9.1.1
|
||||
title: ""
|
||||
date: 2024-10-09T08:24:06.706Z
|
||||
version: 22.0.5
|
||||
title: ""
|
||||
date: 2025-05-29T15:47:31.891Z
|
||||
- commits:
|
||||
- subject: "tests: Replace request with got"
|
||||
hash: a7a408a5c7dcf06b770e8df85e250bfed5a09f75
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Otavio Jacobi
|
||||
nested: []
|
||||
- subject: "deploy-legacy: Replace request with got"
|
||||
hash: e5877c7de917e377082328ee8ab0b502593c9719
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Otavio Jacobi
|
||||
nested: []
|
||||
version: 22.0.4
|
||||
title: ""
|
||||
date: 2025-05-29T13:19:01.228Z
|
||||
- commits:
|
||||
- subject: Bump sentry to v9
|
||||
hash: 5ccee0e4f1ce3bae6963630963cbd72c9c738f77
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Otavio Jacobi
|
||||
nested: []
|
||||
version: 22.0.3
|
||||
title: ""
|
||||
date: 2025-05-29T12:09:56.743Z
|
||||
- commits:
|
||||
- subject: Fix balena build to work with --nologs
|
||||
hash: 8d10c1af2a8eddfa146e2d23161c079127eb5546
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Otavio Jacobi
|
||||
nested: []
|
||||
version: 22.0.2
|
||||
title: ""
|
||||
date: 2025-05-28T19:32:15.230Z
|
||||
- commits:
|
||||
- subject: "DeviceAPI: Move away from `request` in favor of BalenaSdk request"
|
||||
hash: 3396ba5a971d2ae16552576d65dff953031f01ee
|
||||
|
61
CHANGELOG.md
61
CHANGELOG.md
@ -4,6 +4,67 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 22.1.1 - 2025-06-19
|
||||
|
||||
* Deploy: Limit the submitted error_message of images that fail to build to 1000 characters [Thodoris Greasidis]
|
||||
|
||||
## 22.1.0 - 2025-06-09
|
||||
|
||||
* Add support for node 22 [Otavio Jacobi]
|
||||
|
||||
<details>
|
||||
<summary> Bump etcher-sdk to v10.0.0 [Otavio Jacobi] </summary>
|
||||
|
||||
> ### etcher-sdk-10.0.0 - 2025-06-02
|
||||
>
|
||||
> * Drop support to node18 and add support to node 22 & 24 [Otavio Jacobi]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
## 22.0.6 - 2025-06-02
|
||||
|
||||
* Remove `request` dependency [myarmolinsky]
|
||||
* Replace `request` usage with `got` [myarmolinsky]
|
||||
|
||||
## 22.0.5 - 2025-05-29
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Bump etcher-sdk to v9.1.4 [Otavio Jacobi] </summary>
|
||||
|
||||
> ### etcher-sdk-9.1.4 - 2025-05-29
|
||||
>
|
||||
> * Run `npm audit fix` which should only do non-breaking changes [Otavio Jacobi]
|
||||
>
|
||||
> ### etcher-sdk-9.1.3 - 2025-02-17
|
||||
>
|
||||
> * Embed config.json with a fixed timestamp to enable consistent checksums [Pagan Gazzard]
|
||||
>
|
||||
> ### etcher-sdk-9.1.2 - 2024-10-09
|
||||
>
|
||||
> * Update dependency unzip-stream to v0.3.2 [SECURITY] [Self-hosted Renovate Bot]
|
||||
>
|
||||
> ### etcher-sdk-9.1.1 - 2024-10-09
|
||||
>
|
||||
> * patch: add EXLOCK flag for windows [Talha Can Havadar]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
## 22.0.4 - 2025-05-29
|
||||
|
||||
* tests: Replace request with got [Otavio Jacobi]
|
||||
* deploy-legacy: Replace request with got [Otavio Jacobi]
|
||||
|
||||
## 22.0.3 - 2025-05-29
|
||||
|
||||
* Bump sentry to v9 [Otavio Jacobi]
|
||||
|
||||
## 22.0.2 - 2025-05-28
|
||||
|
||||
* Fix balena build to work with --nologs [Otavio Jacobi]
|
||||
|
||||
## 22.0.1 - 2025-05-28
|
||||
|
||||
* DeviceAPI: Move away from `request` in favor of BalenaSdk request [myarmolinsky]
|
||||
|
@ -77,8 +77,8 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
|
||||
The npm installation involves building native (platform-specific) binary modules, which require
|
||||
some development tools to be installed first, as follows.
|
||||
|
||||
> **The balena CLI currently requires Node.js version ^20.6.0**
|
||||
> **Versions 21 and later are not yet fully supported.**
|
||||
> **The balena CLI currently requires Node.js version >=20.6.0**
|
||||
> **Versions 23 and later are not yet fully supported.**
|
||||
|
||||
### Install development tools
|
||||
|
||||
@ -88,7 +88,7 @@ some development tools to be installed first, as follows.
|
||||
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 20
|
||||
$ nvm install 22
|
||||
```
|
||||
|
||||
The `curl` command line above uses
|
||||
@ -105,7 +105,7 @@ recommended.
|
||||
```sh
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 20
|
||||
$ nvm install 22
|
||||
```
|
||||
|
||||
#### **Windows** (not WSL)
|
||||
@ -113,7 +113,7 @@ $ nvm install 20
|
||||
Install:
|
||||
|
||||
* If you'd like the ability to switch between Node.js versions, install
|
||||
- Node.js v20 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||
- Node.js v22 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||
instead.
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||
|
2089
npm-shrinkwrap.json
generated
2089
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "22.0.1",
|
||||
"version": "22.1.1",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -72,7 +72,7 @@
|
||||
"author": "Balena Inc. (https://balena.io/)",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.6.0"
|
||||
"node": ">=20.6.0 <23"
|
||||
},
|
||||
"oclif": {
|
||||
"bin": "balena",
|
||||
@ -124,7 +124,6 @@
|
||||
"@types/node-cleanup": "^2.1.2",
|
||||
"@types/prettyjson": "^0.0.33",
|
||||
"@types/progress-stream": "^2.0.2",
|
||||
"@types/request": "^2.48.7",
|
||||
"@types/rewire": "^2.5.30",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.9",
|
||||
@ -169,9 +168,9 @@
|
||||
"@balena/env-parsing": "^1.1.8",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@oclif/core": "^4.1.0",
|
||||
"@sentry/node": "^6.16.1",
|
||||
"@sentry/node": "^9.0.0",
|
||||
"balena-config-json": "^4.2.7",
|
||||
"balena-device-init": "^8.1.3",
|
||||
"balena-device-init": "^8.1.11",
|
||||
"balena-errors": "^4.7.3",
|
||||
"balena-image-fs": "^7.5.2",
|
||||
"balena-preload": "^18.0.4",
|
||||
@ -192,7 +191,7 @@
|
||||
"docker-progress": "^5.1.3",
|
||||
"dockerode": "^4.0.5",
|
||||
"ejs": "^3.1.6",
|
||||
"etcher-sdk": "9.1.0",
|
||||
"etcher-sdk": "^10.0.0",
|
||||
"express": "^4.17.2",
|
||||
"fast-boot2": "^1.1.0",
|
||||
"fast-levenshtein": "^3.0.0",
|
||||
@ -220,9 +219,8 @@
|
||||
"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.1",
|
||||
"resin-cli-form": "^4.0.0",
|
||||
"resin-cli-visuals": "^3.0.0",
|
||||
"resin-doodles": "^0.2.0",
|
||||
"resin-stream-logger": "^0.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
@ -250,6 +248,6 @@
|
||||
}
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2025-05-28T17:00:46.792Z"
|
||||
"publishedAt": "2025-06-19T09:32:53.877Z"
|
||||
}
|
||||
}
|
||||
|
12
src/app.ts
12
src/app.ts
@ -34,18 +34,14 @@ export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
autoSessionTracking: false,
|
||||
dsn: config.sentryDsn,
|
||||
release: packageJSON.version,
|
||||
});
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setExtras({
|
||||
is_pkg: !!(process as any).pkg,
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
});
|
||||
Sentry.getCurrentScope().setExtras({
|
||||
is_pkg: !!(process as any).pkg,
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
});
|
||||
return Sentry.getCurrentHub();
|
||||
});
|
||||
|
||||
async function checkNodeVersion() {
|
||||
|
@ -38,11 +38,11 @@ import { stripIndent } from './utils/lazy';
|
||||
export async function trackCommand(commandSignature: string) {
|
||||
try {
|
||||
let Sentry: typeof import('@sentry/node');
|
||||
let scope: import('@sentry/node').Scope;
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry = await import('@sentry/node');
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setExtra('command', commandSignature);
|
||||
});
|
||||
scope = Sentry.getCurrentScope();
|
||||
scope.setExtra('command', commandSignature);
|
||||
}
|
||||
const { getCachedUsername } = await import('./utils/bootstrap');
|
||||
let username: string | undefined;
|
||||
@ -52,11 +52,9 @@ export async function trackCommand(commandSignature: string) {
|
||||
// ignore
|
||||
}
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry!.configureScope((scope) => {
|
||||
scope.setUser({
|
||||
id: username,
|
||||
username,
|
||||
});
|
||||
scope!.setUser({
|
||||
id: username,
|
||||
username,
|
||||
});
|
||||
}
|
||||
if (
|
||||
|
@ -1322,6 +1322,9 @@ async function pushAndUpdateServiceImages(
|
||||
}
|
||||
}
|
||||
|
||||
// Error messages are limited to 300KB characters in the API, so we truncate longer ones.
|
||||
const MAX_ERROR_MESSAGE_LENGTH = 300_000;
|
||||
|
||||
async function pushServiceImages(
|
||||
docker: Dockerode,
|
||||
logger: Logger,
|
||||
@ -1344,23 +1347,34 @@ async function pushServiceImages(
|
||||
delete serviceImage.build_log;
|
||||
}
|
||||
|
||||
await releaseMod.updateImage(
|
||||
pineClient,
|
||||
serviceImage.id,
|
||||
// These are the only update-able image fields in bC atm, and passing
|
||||
// the whole image object in v7+ would result the allowlist to reject the request.
|
||||
_.pick(serviceImage, [
|
||||
'end_timestamp',
|
||||
'project_type',
|
||||
'error_message',
|
||||
'build_log',
|
||||
'push_timestamp',
|
||||
'status',
|
||||
'content_hash',
|
||||
'dockerfile',
|
||||
'image_size',
|
||||
]),
|
||||
);
|
||||
// These are the only update-able image fields in bC atm, and passing
|
||||
// the whole image object in v7+ would result the allowlist to reject the request.
|
||||
const imagePayload = _.pick(serviceImage, [
|
||||
'end_timestamp',
|
||||
'project_type',
|
||||
'error_message',
|
||||
'build_log',
|
||||
'push_timestamp',
|
||||
'status',
|
||||
'content_hash',
|
||||
'dockerfile',
|
||||
'image_size',
|
||||
]);
|
||||
|
||||
if (
|
||||
typeof imagePayload.error_message === 'string' &&
|
||||
imagePayload.error_message.length > MAX_ERROR_MESSAGE_LENGTH
|
||||
) {
|
||||
logger.logDebug(
|
||||
`Truncating error message of image ${serviceImage.is_stored_at__image_location} to ${MAX_ERROR_MESSAGE_LENGTH} characters.`,
|
||||
);
|
||||
imagePayload.error_message = imagePayload.error_message.substring(
|
||||
0,
|
||||
MAX_ERROR_MESSAGE_LENGTH,
|
||||
);
|
||||
}
|
||||
|
||||
await releaseMod.updateImage(pineClient, serviceImage.id, imagePayload);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1596,7 +1610,9 @@ function buildProgressAdapter(inline: boolean) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!str.startsWith('Successfully tagged ')) {
|
||||
// We want to keep the regex match instead of startsWith as it also works with buffers
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
|
||||
if (!/^Successfully tagged /.test(str)) {
|
||||
const match = stepRegex.exec(str);
|
||||
if (match) {
|
||||
step = match[1];
|
||||
|
@ -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;
|
||||
@ -106,10 +109,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,10 +117,9 @@ const uploadImage = function (
|
||||
appName: string,
|
||||
logger: Logger,
|
||||
): Promise<{ buildId: number }> {
|
||||
const request = require('request') as typeof import('request');
|
||||
const progressStream =
|
||||
require('progress-stream') as typeof import('progress-stream');
|
||||
const zlib = require('zlib') as typeof import('zlib');
|
||||
const { default: got } = await import('got');
|
||||
const progressStream = await import('progress-stream');
|
||||
const zlib = await import('zlib');
|
||||
|
||||
// Need to strip off the newline
|
||||
const progressMessage = logger
|
||||
@ -141,25 +140,26 @@ 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,14 +167,14 @@ 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 got.post(getBuilderLogPushEndpoint(url, buildId, username, appName), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: Buffer.from(logs),
|
||||
responseType: 'json',
|
||||
throwHttpErrors: false,
|
||||
});
|
||||
};
|
||||
|
||||
@ -232,7 +232,7 @@ export const deployLegacy = async function (
|
||||
username,
|
||||
appName,
|
||||
]);
|
||||
uploadLogs(...args);
|
||||
await uploadLogs(...args);
|
||||
}
|
||||
|
||||
return buildId;
|
||||
|
@ -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 as Error);
|
||||
}
|
||||
});
|
||||
request(qemuUrl)
|
||||
got.stream
|
||||
.get(qemuUrl)
|
||||
.on('error', reject)
|
||||
.pipe(zlib.createGunzip())
|
||||
.on('error', reject)
|
||||
|
@ -16,7 +16,8 @@ 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 type { PlainResponse } from 'got';
|
||||
import type got from 'got';
|
||||
import type { RegistrySecrets } from '@balena/compose/dist/multibuild';
|
||||
import type * as Stream from 'stream';
|
||||
import streamToPromise = require('stream-to-promise');
|
||||
@ -119,7 +120,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);
|
||||
@ -337,32 +338,29 @@ 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(
|
||||
async function createRemoteBuildRequest(
|
||||
build: RemoteBuild,
|
||||
tarStream: Stream.Readable,
|
||||
builderUrl: string,
|
||||
onError: (error: Error) => void,
|
||||
): request.Request {
|
||||
const zlib = require('zlib') as typeof import('zlib');
|
||||
) {
|
||||
const { default: got } = await import('got');
|
||||
const zlib = await 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: {
|
||||
Authorization: `Bearer ${build.auth}`,
|
||||
'Content-Encoding': 'gzip',
|
||||
},
|
||||
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: PlainResponse) => {
|
||||
if (response.statusCode >= 100 && response.statusCode < 400) {
|
||||
if (DEBUG_MODE) {
|
||||
console.error(
|
||||
@ -374,8 +372,8 @@ function createRemoteBuildRequest(
|
||||
'Remote builder responded with HTTP error:',
|
||||
`${response.statusCode} ${response.statusMessage}`,
|
||||
];
|
||||
if (response.body) {
|
||||
msgArr.push(response.body);
|
||||
if (response.rawBody) {
|
||||
msgArr.push(response.rawBody.toString());
|
||||
}
|
||||
onError(new ExpectedError(msgArr.join('\n')));
|
||||
}
|
||||
@ -384,7 +382,7 @@ function createRemoteBuildRequest(
|
||||
|
||||
async function getRemoteBuildStream(
|
||||
build: RemoteBuild,
|
||||
): Promise<[request.Request, Stream.Stream]> {
|
||||
): Promise<[ReturnType<typeof got.stream.post>, Stream.Stream]> {
|
||||
const builderUrl = await getBuilderEndpoint(
|
||||
build.baseUrl,
|
||||
build.appSlug,
|
||||
@ -412,7 +410,7 @@ async function getRemoteBuildStream(
|
||||
}
|
||||
|
||||
const tarStream = await getTarStream(build);
|
||||
const buildRequest = createRemoteBuildRequest(
|
||||
const buildRequest = await createRemoteBuildRequest(
|
||||
build,
|
||||
tarStream,
|
||||
builderUrl,
|
||||
|
@ -20,7 +20,7 @@ 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';
|
||||
@ -61,38 +61,30 @@ describe('Login server:', function () {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
async function testLogin(opt: {
|
||||
async function testLogin({
|
||||
verb = 'post',
|
||||
...opt
|
||||
}: {
|
||||
expectedBody: string;
|
||||
expectedErrorMsg?: string;
|
||||
expectedStatusCode: number;
|
||||
expectedToken: string;
|
||||
urlPath?: string;
|
||||
verb?: string;
|
||||
verb?: 'post' | 'put';
|
||||
}) {
|
||||
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 res = await got[verb](
|
||||
`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 as Error);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
throwHttpErrors: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(res.body).to.equal(opt.expectedBody);
|
||||
expect(res.statusCode).to.equal(opt.expectedStatusCode);
|
||||
|
||||
try {
|
||||
const token = await server.awaitForToken();
|
||||
@ -127,7 +119,7 @@ describe('Login server:', function () {
|
||||
expectedStatusCode: 404,
|
||||
expectedToken: tokens.johndoe.token,
|
||||
expectedErrorMsg: 'Unknown path or verb',
|
||||
verb: 'get',
|
||||
verb: 'put',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -442,6 +442,93 @@ describe('balena build', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should create the expected tar stream (docker-compose --nologs)', async () => {
|
||||
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
|
||||
const service1Dockerfile = (
|
||||
await fs.readFile(
|
||||
path.join(projectPath, 'service1', 'Dockerfile.template'),
|
||||
'utf8',
|
||||
)
|
||||
).replace('%%BALENA_MACHINE_NAME%%', 'nuc');
|
||||
const expectedFilesByService: ExpectedTarStreamFilesByService = {
|
||||
service1: {
|
||||
Dockerfile: {
|
||||
contents: service1Dockerfile,
|
||||
fileSize: service1Dockerfile.length,
|
||||
type: 'file',
|
||||
},
|
||||
'Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||
'file1.sh': { fileSize: 12, type: 'file' },
|
||||
},
|
||||
service2: {
|
||||
'Dockerfile-alt': { fileSize: 13, type: 'file' },
|
||||
'file2-crlf.sh': {
|
||||
fileSize: isWindows ? 12 : 14,
|
||||
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
||||
type: 'file',
|
||||
},
|
||||
'src/file1.sh': { fileSize: 12, type: 'file' },
|
||||
},
|
||||
};
|
||||
const responseFilename = 'build-POST.json';
|
||||
const responseBody = await fs.readFile(
|
||||
path.join(dockerResponsePath, responseFilename),
|
||||
'utf8',
|
||||
);
|
||||
const expectedQueryParamsByService = {
|
||||
service1: Object.entries(
|
||||
_.merge({}, commonComposeQueryParams, {
|
||||
buildargs: {
|
||||
COMPOSE_ARG: 'A',
|
||||
barg: 'b',
|
||||
SERVICE1_VAR: 'This is a service specific variable',
|
||||
},
|
||||
cachefrom: ['my/img1', 'my/img2'],
|
||||
}),
|
||||
),
|
||||
service2: Object.entries(
|
||||
_.merge({}, commonComposeQueryParamsIntel, {
|
||||
buildargs: {
|
||||
COMPOSE_ARG: 'A',
|
||||
barg: 'b',
|
||||
},
|
||||
cachefrom: ['my/img1', 'my/img2'],
|
||||
dockerfile: 'Dockerfile-alt',
|
||||
}),
|
||||
),
|
||||
};
|
||||
const expectedResponseLines: string[] = [
|
||||
...commonResponseLines[responseFilename],
|
||||
...getDockerignoreWarn1(
|
||||
[path.join(projectPath, 'service2', '.dockerignore')],
|
||||
'build',
|
||||
),
|
||||
];
|
||||
if (isWindows) {
|
||||
expectedResponseLines.push(
|
||||
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
|
||||
projectPath,
|
||||
'service2',
|
||||
'file2-crlf.sh',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
docker.expectGetInfo({});
|
||||
docker.expectGetManifestNucAlpine();
|
||||
docker.expectGetManifestBusybox();
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2 --nologs`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService,
|
||||
expectedQueryParamsByService,
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
responseBody,
|
||||
responseCode: 200,
|
||||
services: ['service1', 'service2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => {
|
||||
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
|
||||
const service1Dockerfile = (
|
||||
|
@ -27,7 +27,7 @@ import * as sinon from 'sinon';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
||||
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
|
||||
import { cleanOutput, runCommand, switchSentry } from '../helpers';
|
||||
import { cleanOutput, runCommand } from '../helpers';
|
||||
import type {
|
||||
ExpectedTarStreamFiles,
|
||||
ExpectedTarStreamFilesByService,
|
||||
@ -262,7 +262,6 @@ describe('balena deploy', function () {
|
||||
});
|
||||
|
||||
it('should update a release with status="failed" on error (single container)', async () => {
|
||||
let sentryStatus: boolean | undefined;
|
||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||
const expectedFiles: ExpectedTarStreamFiles = {
|
||||
'src/.dockerignore': { fileSize: 16, type: 'file' },
|
||||
@ -319,7 +318,6 @@ describe('balena deploy', function () {
|
||||
api.expectPostImageLabel();
|
||||
|
||||
try {
|
||||
sentryStatus = await switchSentry(false);
|
||||
sinon.stub(process, 'exit');
|
||||
|
||||
await testDockerBuildStream({
|
||||
@ -337,9 +335,8 @@ describe('balena deploy', function () {
|
||||
});
|
||||
expect(failedImagePatchRequests).to.equal(maxRequestRetries);
|
||||
} finally {
|
||||
await switchSentry(sentryStatus);
|
||||
// @ts-expect-error claims restore does not exist
|
||||
process.exit.restore();
|
||||
// We mock process.exit and need to force cast it to a SinonStub to restore it
|
||||
(process.exit as unknown as sinon.SinonStub).restore();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -417,15 +417,3 @@ export function deepJsonParse(data: any): any {
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function switchSentry(
|
||||
enabled: boolean | undefined,
|
||||
): Promise<boolean | undefined> {
|
||||
const balenaCLI = await import('../build/app');
|
||||
const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions();
|
||||
if (sentryOpts) {
|
||||
const sentryStatus = sentryOpts.enabled;
|
||||
sentryOpts.enabled = enabled;
|
||||
return sentryStatus;
|
||||
}
|
||||
}
|
||||
|
@ -41,23 +41,18 @@ export class BuilderMock extends NockMock {
|
||||
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
|
||||
}) {
|
||||
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(
|
||||
async function (uri, requestBody, callback) {
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await opts.checkURI(uri);
|
||||
if (typeof requestBody === 'string') {
|
||||
const gzipped = Buffer.from(requestBody, 'hex');
|
||||
const gunzipped = await gunzipAsync(gzipped);
|
||||
await opts.checkBuildRequestBody(gunzipped);
|
||||
} else {
|
||||
throw new Error(
|
||||
`unexpected requestBody type "${typeof requestBody}"`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
async function (uri, requestBody) {
|
||||
await opts.checkURI(uri);
|
||||
if (typeof requestBody === 'string') {
|
||||
const gzipped = Buffer.from(requestBody, 'hex');
|
||||
const gunzipped = await gunzipAsync(gzipped);
|
||||
await opts.checkBuildRequestBody(gunzipped);
|
||||
return [opts.responseCode, opts.responseBody];
|
||||
} else {
|
||||
throw new Error(
|
||||
`unexpected requestBody type "${typeof requestBody}"`,
|
||||
);
|
||||
}
|
||||
callback(error, [opts.responseCode, opts.responseBody]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -81,21 +81,14 @@ export class DockerMock extends NockMock {
|
||||
this.optPost(
|
||||
new RegExp(`^/build\\?(|.+&)${qs.stringify({ t: opts.tag })}&`),
|
||||
opts,
|
||||
).reply(async function (uri, requestBody, cb) {
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await opts.checkURI(uri);
|
||||
if (typeof requestBody === 'string') {
|
||||
await opts.checkBuildRequestBody(requestBody);
|
||||
} else {
|
||||
throw new Error(
|
||||
`unexpected requestBody type "${typeof requestBody}"`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
).reply(async function (uri, requestBody) {
|
||||
await opts.checkURI(uri);
|
||||
if (typeof requestBody === 'string') {
|
||||
await opts.checkBuildRequestBody(requestBody);
|
||||
return [opts.responseCode, opts.responseBody];
|
||||
} else {
|
||||
throw new Error(`unexpected requestBody type "${typeof requestBody}"`);
|
||||
}
|
||||
cb(error, [opts.responseCode, opts.responseBody]);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ describe('detectEncoding() function', function () {
|
||||
it('should correctly detect the encoding of a few selected files', async () => {
|
||||
const sampleBinary = [
|
||||
'drivelist/build/Release/drivelist.node',
|
||||
'mountutils/build/Release/MountUtils.node',
|
||||
'mountutils/prebuilds/linux-x64/mountutils.node',
|
||||
];
|
||||
const sampleText = [
|
||||
'node_modules/.bin/mocha',
|
||||
|
Reference in New Issue
Block a user