Compare commits

...

54 Commits

Author SHA1 Message Date
7f8106a64b WIP 2022-05-03 21:25:32 +00:00
ba3a3865b5 v13.4.1 2022-04-29 04:45:30 +03:00
f8402bc40c Merge pull request #2444 from balena-io/balena-leave-clearer-message
patch: Tell user that balena leave command does not remove the device…
2022-04-11 16:33:26 +00:00
c667ffa8eb leave: Update log message to advise that device still needs deleting
Change-type: patch
2022-04-11 17:04:45 +01:00
6d6065ddf5 v13.4.0 2022-04-11 17:18:27 +03:00
44f55f8e7b Merge pull request #2473 from balena-io/2337-support-all-valid-sermer-on-releases
deploy: Support all valid semver versions in balena.yml
2022-04-11 14:05:16 +00:00
d2c77760b3 deploy: Support all valid semver versions in balena.yml
Resolves: #2337
Change-type: minor
Depends-on: https://github.com/balena-io/open-balena-api/pull/982
Depends-on: https://github.com/balena-io/balena-api/pull/3584
See: https://jel.ly.fish/product-improvement-draft-releases-and-release-versioning-d0391f45-c2f9-4f4e-b964-1a7e9023a3f4
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-04-08 18:16:45 +03:00
7496710c85 v13.3.3 2022-04-08 14:39:33 +03:00
be6a468507 Merge pull request #2471 from balena-io/patches-contributing
Document the 'patches' folder in CONTRIBUTING.md
2022-04-08 11:36:49 +00:00
88835e63bd Document the 'patches' folder in CONTRIBUTING.md
Change-type: patch
2022-04-08 01:16:28 +01:00
3572cb3cd6 v13.3.2 2022-04-07 13:25:31 +03:00
7fbd1de063 Merge pull request #2470 from balena-io/2469-build-docker-tls
build: Ensure HTTPS is used with dockerPort 2376 or with ca/cert/key
2022-04-07 10:22:54 +00:00
a4ab07cd08 Skip Alpine tests until Concourse + Alpine v3.14 issues are resolved
See:
* https://github.com/concourse/concourse/issues/7905
* https://github.com/product-os/balena-concourse/issues/631
* https://github.com/product-os/ci-images/pull/116/files#r844508619

Change-type: patch
2022-04-07 00:29:55 +01:00
9185eaa2b7 build: Ensure HTTPS is used with dockerPort 2376 or with ca/cert/key
Change-type: patch
2022-04-07 00:14:03 +01:00
ff3abe1fba v13.3.1 2022-03-08 22:33:20 +02:00
1ac3b70b81 Merge pull request #2463 from balena-io/update-notifier-release-notes
Include link to Wiki release notes in version update notifications
2022-03-08 20:31:39 +00:00
e946178953 Include link to Wiki release notes in version update notifications
Change-type: patch
2022-03-08 18:25:08 +00:00
6589589bee v13.3.0 2022-03-01 00:35:10 +02:00
6ae598b55e Merge pull request #2461 from balena-io/2458-ssh-ipaddr-service
ssh: Allow ssh to service with IP address and production balenaOS image
2022-02-28 22:33:02 +00:00
915f7e3763 ssh: Allow ssh to service with IP address and production balenaOS image
Also remove 'balena ssh' dependency on the device supervisor (that may
be down because of device issues or a supervisor bug) when opening a
ssh shell on a container (#1560).

Resolves: #2458
Resolves: #1560
Change-type: minor
2022-02-28 21:39:49 +00:00
cd17d79067 ssh: Advise use of 'balena login' if root authentication fails
Change-type: patch
2022-02-24 21:48:40 +00:00
7e4f4392e9 v13.2.1 2022-02-24 23:45:39 +02:00
3c0e998616 Merge pull request #2460 from balena-io/uuid-log
Correctly use the device uuid when logging the tunnel target
2022-02-24 21:43:28 +00:00
bd1bf8153d Remove unnecessary fetch of device info in balena tunnel
Change-type: patch
2022-02-24 21:02:27 +00:00
f2528dcd18 Correctly use the device uuid when logging the tunnel target
The "vpn address" is only relevant on the device/vpn server themselves
and makes no sense from a CLI context as it uses the uuid to specify
the target

Change-type: patch
2022-02-24 21:00:58 +00:00
ec26433925 v13.2.0 2022-02-18 23:40:57 +02:00
43cddd2e5d Merge pull request #2457 from balena-io/2292-ssh-username
ssh: Attempt cloud username if 'root' authentication fails
2022-02-18 21:38:27 +00:00
eeb2be2912 ssh: Attempt cloud username if 'root' authentication fails
Also refactor several files to avoid code duplication.

Change-type: minor
2022-02-12 02:40:35 +00:00
3bf8befb1d Replace occurrence of through2 dependency with standard stream module
Change-type: patch
2022-02-11 17:04:32 +00:00
948095ce4d Refactor cached username logic from events.ts to bootstrap.ts for reuse
Change-type: patch
2022-02-11 15:23:36 +00:00
d2330f9ed1 v13.1.13 2022-02-10 14:29:18 +02:00
cc19b00998 Merge pull request #2455 from balena-io/lucianbuzzo/drop-unused-fn-awaitdevice
Drop unused awaitDevice utility function
2022-02-10 12:27:25 +00:00
ed5ac75a10 v13.1.12 2022-02-09 09:24:17 +02:00
465b8a1b5e Merge pull request #2451 from balena-io/bump-preload-v12
Update balena-preload to v12
2022-02-09 07:22:04 +00:00
eccadbdcb9 Drop unused awaitDevice utility function
Change-type: patch
Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
2022-02-01 17:43:28 +00:00
31eb734af1 Update balena-preload to v12
Update balena-preload from 11.0.0 to 12.0.0

Change-type: patch
Changelog-entry: preload: Stop using the deprecated /device-types/v1 API endpoints
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-01-27 19:27:58 +02:00
fa7b59d64f v13.1.11 2022-01-20 01:55:52 +02:00
1e42bfa0d5 Merge pull request #2450 from balena-io/types-node-v12
chore: Update @types/node to v12.20.42
2022-01-19 23:53:08 +00:00
5464e550e7 chore: lib/auth/utils.ts: Replace deprecated url.resolve, use async/await
Change-type: patch
2022-01-19 22:48:46 +00:00
c0f27a663d chore: Update @types/node to v12.20.42
Change-type: patch
2022-01-19 22:48:46 +00:00
d1c61c62ab v13.1.10 2022-01-16 21:29:44 +02:00
a9691bff57 Merge pull request #2446 from balena-io/2445-min-node-version-12.8.0
Update docs and package.json re min Node.js supported version (12.8.0)
2022-01-16 19:27:33 +00:00
f5d09a43cd Update docs and package.json re min Node.js supported version (12.8.0)
Resolves: #2445
Change-type: patch
2022-01-16 18:44:45 +00:00
d11e547e11 v13.1.9 2022-01-14 03:00:53 +02:00
bd462aee02 Merge pull request #2443 from balena-io/colors-action
Update packages in response to colors package issues
2022-01-14 00:58:59 +00:00
f633c0468b Update packages in response to colors package issues
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2022-01-12 22:12:06 +01:00
e4f61a1242 v13.1.8 2022-01-11 03:28:31 +02:00
96142a002e Merge pull request #2442 from balena-io/2440-pin-docker-modem
local push: Fix "invalid character '/' looking for beginning of value"
2022-01-11 01:26:51 +00:00
6b9a5cd89c local push: Fix "invalid character '/' looking for beginning of value"
Change-type: patch
2022-01-11 00:15:10 +00:00
ba2d3d60ec Merge pull request #2441 from balena-io/v14-TypeError-type-undefined
v14 preparations: Fix TypeError produced by 'npx oclif manifest'
2022-01-08 01:55:40 +00:00
d1e66bc1a5 v14 preparations: Fix TypeError produced by 'npx oclif manifest'
Change-type: patch
2022-01-08 01:16:45 +00:00
58799915a9 v13.1.7 2022-01-06 19:29:35 +02:00
5f2d55f569 Merge pull request #2436 from balena-io/update-pkg
Update to pkg 5
2022-01-06 17:27:47 +00:00
879d98ef98 Update to pkg 5
Change-type: patch
2022-01-04 16:31:08 +00:00
42 changed files with 6073 additions and 4550 deletions

View File

@ -7,9 +7,14 @@ npm:
node_versions:
- "12"
- "14"
- name: linux
os: alpine
architecture: x86_64
node_versions:
- "12"
- "14"
##
## Temporarily skip Alpine tests until the following issues are resolved:
## * https://github.com/concourse/concourse/issues/7905
## * https://github.com/product-os/balena-concourse/issues/631
##
# - name: linux
# os: alpine
# architecture: x86_64
# node_versions:
# - "12"
# - "14"

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,293 @@ 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/).
## 13.4.1 - 2022-04-11
* leave: Update log message to advise that device still needs deleting [Taro Murao]
## 13.4.0 - 2022-04-08
* deploy: Support all valid semver versions in balena.yml [Thodoris Greasidis]
## 13.3.3 - 2022-04-08
* Document the 'patches' folder in CONTRIBUTING.md [Paulo Castro]
## 13.3.2 - 2022-04-07
* Skip Alpine tests until Concourse + Alpine v3.14 issues are resolved [Paulo Castro]
* build: Ensure HTTPS is used with dockerPort 2376 or with ca/cert/key [Paulo Castro]
## 13.3.1 - 2022-03-08
* Include link to Wiki release notes in version update notifications [Paulo Castro]
## 13.3.0 - 2022-02-28
* ssh: Allow ssh to service with IP address and production balenaOS image [Paulo Castro]
* ssh: Advise use of 'balena login' if root authentication fails [Paulo Castro]
## 13.2.1 - 2022-02-24
* Remove unnecessary fetch of device info in `balena tunnel` [Pagan Gazzard]
* Correctly use the device uuid when logging the tunnel target [Pagan Gazzard]
## 13.2.0 - 2022-02-12
* ssh: Attempt cloud username if 'root' authentication fails [Paulo Castro]
* Replace occurrence of through2 dependency with standard stream module [Paulo Castro]
* Refactor cached username logic from events.ts to bootstrap.ts for reuse [Paulo Castro]
## 13.1.13 - 2022-02-10
* Drop unused awaitDevice utility function [Lucian Buzzo]
## 13.1.12 - 2022-02-09
<details>
<summary> preload: Stop using the deprecated /device-types/v1 API endpoints [Thodoris Greasidis] </summary>
> ### balena-preload-12.0.0 - 2022-01-27
>
> * Improve types [Thodoris Greasidis]
> * Stop relying on the /device-types/v1 endpoints [Thodoris Greasidis]
> * Bump TypeScript to v4.5 [Thodoris Greasidis]
>
> <details>
> <summary> Bump balena-sdk to v16 [Thodoris Greasidis] </summary>
>
>> #### balena-sdk-16.0.0 - 2021-11-28
>>
>> * **BREAKING**: Merge the hostApp model into the OS model [Thodoris Greasidis]
>> * **BREAKING** Drop os.getSupportedVersions() method in favor of hostapp.getAvailableOsVersions() [Thodoris Greasidis]
>> * os.getMaxSatisfyingVersion: Add optional param to choose OS line type [Thodoris Greasidis]
>> * os.getMaxSatisfyingVersion: Include ESR versions [Thodoris Greasidis]
>> * os.getMaxSatisfyingVersion: Switch to use hostApps [Thodoris Greasidis]
>> * hostapp.getAvailableOsVersions: Add single device type argument overload [Thodoris Greasidis]
>> * hostapp.getAllOsVersions: Add single device type argument overload [Thodoris Greasidis]
>> * models.hostapp: Add a getAvailableOsVersions() convenience method [Thodoris Greasidis]
>> * Support optional extra PineOptions in hostapp.getAllOsVersions() [Thodoris Greasidis]
>> * **BREAKING** Include invalidated versions in hostapp.getAllOsVersions() [Thodoris Greasidis]
>> * models/application: Add getDirectlyAccessible & getAllDirectlyAccessible [Thodoris Greasidis]
>> * application.get: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
>> * application.getAll: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
>> * **BREAKING** Change application.getAll to include public apps [Thodoris Greasidis]
>> * **BREAKING** Drop targeting/retrieving apps by name in favor of slugs [Thodoris Greasidis]
>> * Bump minimum supported Typescript to v4.5.2 [Thodoris Greasidis]
>> * **BREAKING**: Stop actively supporting node 10 [Thodoris Greasidis]
>> * **BREAKING** Drop application.getAllWithDeviceServiceDetails() [Thodoris Greasidis]
>> * **BREAKING** Change apiKey.getAll() to return all key variants [Thodoris Greasidis]
>> * types: Drop is_in_local_mode from the Device model [Thodoris Greasidis]
>> * types: Drop user__is_member_of__application in favor of the term form [Thodoris Greasidis]
>> * typings: Drop Subscription's discounts__plan_addon property [Thodoris Greasidis]
>> * typings: Stop extending the JWTUser type in the User model [Thodoris Greasidis]
>> * models/config: Change the BETA device type state to NEW [Thodoris Greasidis]
>> * typings: Drop the PineWithSelectOnGet type [Thodoris Greasidis]
>> * Remove my_application from the supported resources [Thodoris Greasidis]
>> * typings: Properly type some Device properties [Thodoris Greasidis]
>> * typings: Drop the DeviceWithImageInstalls type [Thodoris Greasidis]
>>
>> #### balena-sdk-15.59.2 - 2021-11-28
>>
>>
>> <details>
>> <summary> Update balena-request to 11.5.0 [Thodoris Greasidis] </summary>
>>
>>> ##### balena-request-11.5.0 - 2021-11-28
>>>
>>> * Convert tests to JavaScript and drop coffeescript [Thodoris Greasidis]
>>> * Fix the jsdoc generation [Thodoris Greasidis]
>>> * Convert to typescript and publish typings [Thodoris Greasidis]
>>>
>> </details>
>>
>>
>> #### balena-sdk-15.59.1 - 2021-11-28
>>
>> * Fix the typings of the Image contract field [Thodoris Greasidis]
>> * Fix the typings for the Release contract field [Thodoris Greasidis]
>>
>> #### balena-sdk-15.59.0 - 2021-11-24
>>
>> * Add release setIsInvalidated function [Matthew Yarmolinsky]
>>
>> #### balena-sdk-15.58.1 - 2021-11-17
>>
>> * Update typescript to 4.5.2 [Thodoris Greasidis]
>>
>> #### balena-sdk-15.58.0 - 2021-11-16
>>
>> * models/release: Add note() method [Thodoris Greasidis]
>> * typings: Add the release.invalidation_reason property [Thodoris Greasidis]
>> * typings: Add the release.note property [Thodoris Greasidis]
>>
>> #### balena-sdk-15.57.2 - 2021-11-15
>>
>> * tests/logs: Increase the wait time for retrieving the subscribed logs [Thodoris Greasidis]
>> * tests/logs: Refactor to async-await [Thodoris Greasidis]
>>
>> #### balena-sdk-15.57.1 - 2021-11-11
>>
>> * typings: Fix $filters for resources with non numeric ids [Thodoris Greasidis]
>> * typings: Add application.can_use__application_as_host ReverseNavigation [Thodoris Greasidis]
>> * Add missing apiKey.getDeviceApiKeysByDevice docs [Thodoris Greasidis]
>>
>> #### balena-sdk-15.57.0 - 2021-11-05
>>
>> * models/api-key: Change update() & revoke() to work with all key variants [Thodoris Greasidis]
>>
>> #### balena-sdk-15.56.0 - 2021-11-04
>>
>> * models/apiKey: Add getDeviceApiKeysByDevice() method [Thodoris Greasidis]
>>
>> #### balena-sdk-15.55.0 - 2021-11-01
>>
>> * typings: Add the release.raw_version property [Thodoris Greasidis]
>>
>> #### balena-sdk-15.54.2 - 2021-10-25
>>
>> * application/create: Rely on the hostApps for detecting discontinued DTs [Thodoris Greasidis]
>>
>> #### balena-sdk-15.54.1 - 2021-10-22
>>
>> * tests/device: Async-await conversions & abstraction on multi-field tests [Thodoris Greasidis]
>>
>> #### balena-sdk-15.54.0 - 2021-10-20
>>
>> * tests: Register devices in chunks of 10 to avoid uuid conflicts in node [Thodoris Greasidis]
>> * Add known issue check on release isReccomanded logic [JSReds]
>> * Add known_issue_list to hostApp.getOsVersions() [JSReds]
>>
>> #### balena-sdk-15.53.0 - 2021-10-07
>>
>> * Add support for batch device supervisor updates [Thodoris Greasidis]
>>
>> #### balena-sdk-15.52.0 - 2021-10-06
>>
>> * Add support for batch device pinning to release [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.4 - 2021-09-28
>>
>> * auth.isLoggedIn: Treat BalenaExpiredToken errors as logged out indicator [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.3 - 2021-09-28
>>
>> * Convert application spec to TypeScript [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.2 - 2021-09-28
>>
>> * application.trackLatestRelease: Fix using draft/invalidated releases [Thodoris Greasidis]
>> * application.isTrackingLatestRelease: Exclude draft&invalidated releases [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.1 - 2021-09-20
>>
>>
>> <details>
>> <summary> Update balena-request to v11.4.2 [Kyle Harding] </summary>
>>
>>> ##### balena-request-11.4.2 - 2021-09-20
>>>
>>> * Allow overriding the default zlib flush setting [Kyle Harding]
>>>
>> </details>
>>
>>
>> #### balena-sdk-15.51.0 - 2021-09-16
>>
>> * os.getConfig: Add typings for the provisioningKeyName option [Nitish Agarwal]
>>
>> #### balena-sdk-15.50.1 - 2021-09-13
>>
>> * models/os: Always first normalize the device type slug [Thodoris Greasidis]
>>
>> #### balena-sdk-15.50.0 - 2021-09-10
>>
>> * Add release.finalize to promote draft releases to final [toochevere]
>>
>> #### balena-sdk-15.49.1 - 2021-09-10
>>
>> * typings: Drop the v5-model-only application_type.is_host_os [Thodoris Greasidis]
>>
>> #### balena-sdk-15.49.0 - 2021-09-06
>>
>> * os.getSupportedOsUpdateVersions: Use the hostApp releases [Thodoris Greasidis]
>> * os.download: Use the hostApp for finding the latest release [Thodoris Greasidis]
>>
>> #### balena-sdk-15.48.3 - 2021-08-27
>>
>>
>> <details>
>> <summary> Update balena-request to 11.4.1 [Kyle Harding] </summary>
>>
>>> ##### balena-request-11.4.1 - 2021-08-27
>>>
>>> * Allow more lenient gzip decompression [Kyle Harding]
>>>
>> </details>
>>
>>
>> #### balena-sdk-15.48.2 - 2021-08-27
>>
>> * Improve hostapp.getAllOsVersions performance & reduce fetched data [Thodoris Greasidis]
>>
>> #### balena-sdk-15.48.1 - 2021-08-27
>>
>> * Update typescript to 4.4.2 [Thodoris Greasidis]
>>
>> #### balena-sdk-15.48.0 - 2021-08-15
>>
>> * Deprecate the release.release_version property [Thodoris Greasidis]
>> * typings: Add the release versioning properties [Thodoris Greasidis]
>>
>> #### balena-sdk-15.47.1 - 2021-08-10
>>
>> * Run browser tests using the minified browser bundle [Thodoris Greasidis]
>> * Move to uglify-js to fix const assignment bug in minified build [Thodoris Greasidis]
>>
>> #### balena-sdk-15.47.0 - 2021-08-09
>>
>> * typings: Add the release.is_final & is_finalized_at__date properties [Thodoris Greasidis]
>>
>> #### balena-sdk-15.46.1 - 2021-07-28
>>
>> * apiKey.getAll: Return only NamedUserApiKeys for backwards compatibility [Thodoris Greasidis]
>>
>> #### balena-sdk-15.46.0 - 2021-07-27
>>
>> * Add email verification & email request methods [Nitish Agarwal]
>>
>> #### balena-sdk-15.45.0 - 2021-07-26
>>
>> * Update generateProvisioningKey to include keyName [Nitish Agarwal]
>>
> </details>
>
>
</details>
## 13.1.11 - 2022-01-19
* chore: lib/auth/utils.ts: Replace deprecated url.resolve, use async/await [Paulo Castro]
* chore: Update @types/node to v12.20.42 [Paulo Castro]
## 13.1.10 - 2022-01-16
* Update docs and package.json re min Node.js supported version (12.8.0) [Paulo Castro]
## 13.1.9 - 2022-01-14
* Update packages in response to `colors` package issues [Scott Lowe]
## 13.1.8 - 2022-01-11
* local push: Fix "invalid character '/' looking for beginning of value" [Paulo Castro]
* v14 preparations: Fix TypeError produced by 'npx oclif manifest' [Paulo Castro]
## 13.1.7 - 2022-01-06
* Update to pkg 5 [Pagan Gazzard]
## 13.1.6 - 2022-01-04
* Automation: enforce noImplicitAny for the type-checked javascript [Pagan Gazzard]

View File

@ -125,6 +125,39 @@ The README file is manually edited, but subsections are automatically extracted
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
## Patches folder
The `patches` folder contains patch files created with the
[patch-package](https://www.npmjs.com/package/patch-package) tool. Small code changes to
third-party modules can be made by directly editing Javascript files under the `node_modules`
folder and then running `patch-package` to create the patch files. The patch files are then
applied immediately after `npm install`, through the `postinstall` script defined in
`package.json`.
The subfolders of the `patches` folder are documented in the
[apply-patches.js](https://github.com/balena-io/balena-cli/blob/master/patches/apply-patches.js)
script.
To make changes to the patch files under the `patches` folder, **do not edit them directly,**
not even for a "single character change" because the hash values in the patch files also need
to be recomputed by `patch-packages`. Instead, edit the relevant files under `node_modules`
directly, and then run `patch-packages` with the `--patch-dir` option to specify the subfolder
where the patch should be saved. For example, edit `node_modules/exit-hook/index.js` and then
run:
```sh
$ npx patch-package --patch-dir patches/all exit-hook
```
That said, these kinds of patches should be avoided in favour of creating pull requests
upstream. Patch files create additional maintenance work over time as the patches need to be
updated when the dependencies are updated, and they prevent the compounding community benefit
that sharing fixes upstream have on open source projects like the balena CLI. The typical
scenario where these patches are used is when the upstream maintainers are unresponsive or
unwilling to merge the required fixes, the fixes are very small and specific to the balena CLI,
and creating a fork of the upstream repo is likely to be more long-term effort than maintaining
the patches.
## Windows
Besides the regular npm installation dependencies, the `npm run build:installer` script

View File

@ -78,7 +78,7 @@ 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 10 (min 10.20.0) or 12.**
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
> **Versions 13 and later are not yet fully supported.**
### Install development tools

View File

@ -36,8 +36,8 @@ const run = async (cmd: string) => {
}
resolve({ stdout, stderr });
});
p.stdout.pipe(process.stdout);
p.stderr.pipe(process.stderr);
p.stdout?.pipe(process.stdout);
p.stderr?.pipe(process.stderr);
});
};

View File

@ -117,7 +117,7 @@ export async function which(program: string): Promise<string> {
*/
export async function whichSpawn(
programName: string,
args?: string[],
args: string[] = [],
): Promise<void> {
const program = await which(programName);
let error: Error | undefined;

View File

@ -3020,6 +3020,10 @@ Use QEMU for ARM architecture emulation during the image build
Alternative Dockerfile name/path, relative to the source folder
#### --dockercompose DOCKERCOMPOSE
Alternative docker-compose.yml name in the source root folder
#### --logs
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
@ -3244,6 +3248,10 @@ Use QEMU for ARM architecture emulation during the image build
Alternative Dockerfile name/path, relative to the source folder
#### --dockercompose DOCKERCOMPOSE
Alternative docker-compose.yml name in the source root folder
#### --logs
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.

View File

@ -14,57 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as _ from 'lodash';
import * as url from 'url';
import { getBalenaSdk } from '../utils/lazy';
/**
* @summary Get dashboard CLI login URL
* @function
* @protected
* Get dashboard CLI login URL
*
* @param {String} callbackUrl - callback url
* @fulfil {String} - dashboard login url
* @returns {Promise}
*
* @example
* utils.getDashboardLoginURL('http://127.0.0.1:3000').then (url) ->
* console.log(url)
* @param callbackUrl - Callback url, e.g. 'http://127.0.0.1:3000'
* @returns Dashboard login URL, e.g.:
* 'https://dashboard.balena-cloud.com/login/cli/http%253A%252F%252F127.0.0.1%253A59581%252Fauth'
*/
export const getDashboardLoginURL = (callbackUrl: string) => {
export async function getDashboardLoginURL(
callbackUrl: string,
): Promise<string> {
// Encode percentages signs from the escaped url
// characters to avoid angular getting confused.
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25');
return getBalenaSdk()
.settings.get('dashboardUrl')
.then((dashboardUrl) =>
url.resolve(dashboardUrl, `/login/cli/${callbackUrl}`),
);
};
const [{ URL }, dashboardUrl] = await Promise.all([
import('url'),
getBalenaSdk().settings.get('dashboardUrl'),
]);
return new URL(`/login/cli/${callbackUrl}`, dashboardUrl).href;
}
/**
* @summary Log in using a token, but only if the token is valid
* @function
* @protected
* Log in using a token, but only if the token is valid.
*
* @description
* This function checks that the token is not only well-structured
* but that it also authenticates with the server successfully.
*
* If authenticated, the token is persisted, if not then the previous
* login state is restored.
*
* @param {String} token - session token or api key
* @fulfil {Boolean} - whether the login was successful or not
* @returns {Promise}
*
* utils.loginIfTokenValid('...').then (loggedIn) ->
* if loggedIn
* console.log('Token is valid!')
* @param token - session token or api key
* @returns whether the login was successful or not
*/
export const loginIfTokenValid = async (token: string): Promise<boolean> => {
if (_.isEmpty(token?.trim())) {
export async function loginIfTokenValid(token?: string): Promise<boolean> {
token = (token || '').trim();
if (!token) {
return false;
}
const balena = getBalenaSdk();
@ -86,4 +73,4 @@ export const loginIfTokenValid = async (token: string): Promise<boolean> => {
}
}
return isLoggedIn;
};
}

View File

@ -57,7 +57,7 @@ export default class ConfigInjectCmd extends Command {
public static usage = 'config inject <file>';
public static flags: flags.Input<FlagsDef> = {
type: cf.deviceTypeIgnored,
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};

View File

@ -47,7 +47,7 @@ export default class ConfigReadCmd extends Command {
public static usage = 'config read';
public static flags: flags.Input<FlagsDef> = {
type: cf.deviceTypeIgnored,
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
json: cf.json,

View File

@ -50,7 +50,7 @@ export default class ConfigReconfigureCmd extends Command {
public static usage = 'config reconfigure';
public static flags: flags.Input<FlagsDef> = {
type: cf.deviceTypeIgnored,
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
advanced: flags.boolean({
description: 'show advanced commands',

View File

@ -64,7 +64,7 @@ export default class ConfigWriteCmd extends Command {
public static usage = 'config write <key> <value>';
public static flags: flags.Input<FlagsDef> = {
type: cf.deviceTypeIgnored,
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};

View File

@ -114,7 +114,7 @@ ${dockerignoreHelp}
];
public static usage = 'deploy <fleet> [image]';
// TODO: docker-compose naming
public static flags: flags.Input<FlagsDef> = {
source: flags.string({
description:

View File

@ -138,6 +138,7 @@ export default class PushCmd extends Command {
char: 'e',
default: false,
}),
// TODO: docker-compose naming
dockerfile: flags.string({
description:
'Alternative Dockerfile name/path, relative to the source folder',

View File

@ -20,7 +20,6 @@ import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { parseAsInteger, validateLocalHostnameOrIp } from '../utils/validation';
import * as BalenaSdk from 'balena-sdk';
interface FlagsDef {
port?: number;
@ -128,8 +127,8 @@ export default class SshCmd extends Command {
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
return await performLocalDeviceSSH({
address: params.fleetOrDevice,
port: options.port,
hostname: params.fleetOrDevice,
port: options.port || 'local',
forceTTY: options.tty,
verbose: options.verbose,
service: params.service,
@ -152,12 +151,6 @@ export default class SshCmd extends Command {
params.fleetOrDevice,
);
const device = await sdk.models.device.get(deviceUuid, {
$select: ['id', 'supervisor_version', 'is_online'],
});
const deviceId = device.id;
const supervisorVersion = device.supervisor_version;
const { which } = await import('../utils/which');
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
@ -209,19 +202,15 @@ export default class SshCmd extends Command {
// that we know exists and is accessible
let containerId: string | undefined;
if (params.service != null) {
containerId = await this.getContainerId(
sdk,
const { getContainerIdForService } = await import('../utils/device/ssh');
containerId = await getContainerIdForService({
deviceUuid,
params.service,
{
port: options.port,
proxyCommand,
proxyUrl: proxyUrl || '',
username: username!,
},
supervisorVersion,
deviceId,
);
hostname: `ssh.${proxyUrl}`,
port: options.port || 'cloud',
proxyCommand,
service: params.service,
username: username!,
});
}
let accessCommand: string;
@ -230,158 +219,14 @@ export default class SshCmd extends Command {
} else {
accessCommand = `host ${deviceUuid}`;
}
const command = this.generateVpnSshCommand({
uuid: deviceUuid,
command: accessCommand,
verbose: options.verbose,
port: options.port,
const { runRemoteCommand } = await import('../utils/ssh');
await runRemoteCommand({
cmd: accessCommand,
hostname: `ssh.${proxyUrl}`,
port: options.port || 'cloud',
proxyCommand,
proxyUrl: proxyUrl || '',
username: username!,
username,
verbose: options.verbose,
});
const { spawnSshAndThrowOnError } = await import('../utils/ssh');
return spawnSshAndThrowOnError(command);
}
async getContainerId(
sdk: BalenaSdk.BalenaSDK,
uuid: string,
serviceName: string,
sshOpts: {
port?: number;
proxyCommand?: string[];
proxyUrl: string;
username: string;
},
version?: string,
id?: number,
): Promise<string> {
const semver = await import('balena-semver');
if (version == null || id == null) {
const device = await sdk.models.device.get(uuid, {
$select: ['id', 'supervisor_version'],
});
version = device.supervisor_version;
id = device.id;
}
let containerId: string | undefined;
if (semver.gte(version, '8.6.0')) {
const apiUrl = await sdk.settings.get('apiUrl');
// TODO: Move this into the SDKs device model
const request = await sdk.request.send({
method: 'POST',
url: '/supervisor/v2/containerId',
baseUrl: apiUrl,
body: {
method: 'GET',
deviceId: id,
},
});
if (request.status !== 200) {
throw new Error(
`There was an error connecting to device ${uuid}, HTTP response code: ${request.status}.`,
);
}
const body = request.body;
if (body.status !== 'success') {
throw new Error(
`There was an error communicating with device ${uuid}.\n\tError: ${body.message}`,
);
}
containerId = body.services[serviceName];
} else {
console.error(stripIndent`
Using legacy method to detect container ID. This will be slow.
To speed up this process, please update your device to an OS
which has a supervisor version of at least v8.6.0.
`);
// We need to execute a balena ps command on the device,
// and parse the output, looking for a specific
// container
const childProcess = await import('child_process');
const { escapeRegExp } = await import('lodash');
const { which } = await import('../utils/which');
const { deviceContainerEngineBinary } = await import(
'../utils/device/ssh'
);
const sshBinary = await which('ssh');
const sshArgs = this.generateVpnSshCommand({
uuid,
verbose: false,
port: sshOpts.port,
command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`,
proxyCommand: sshOpts.proxyCommand,
proxyUrl: sshOpts.proxyUrl,
username: sshOpts.username,
});
if (process.env.DEBUG) {
console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`);
}
const subProcess = childProcess.spawn(sshBinary, sshArgs, {
stdio: [null, 'pipe', null],
});
const containers = await new Promise<string>((resolve, reject) => {
const output: string[] = [];
subProcess.stdout.on('data', (chunk) => output.push(chunk.toString()));
subProcess.on('close', (code: number) => {
if (code !== 0) {
reject(
new Error(
`Non-zero error code when looking for service container: ${code}`,
),
);
} else {
resolve(output.join(''));
}
});
});
const lines = containers.split('\n');
const regex = new RegExp(`\\/?${escapeRegExp(serviceName)}_\\d+_\\d+`);
for (const container of lines) {
const [cId, name] = container.split(' ');
if (regex.test(name)) {
containerId = cId;
break;
}
}
}
if (containerId == null) {
throw new Error(
`Could not find a service ${serviceName} on device ${uuid}.`,
);
}
return containerId;
}
generateVpnSshCommand(opts: {
uuid: string;
command: string;
verbose: boolean;
port?: number;
username: string;
proxyUrl: string;
proxyCommand?: string[];
}) {
return [
...(opts.verbose ? ['-vvv'] : []),
'-t',
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
...(opts.proxyCommand && opts.proxyCommand.length
? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`]
: []),
...(opts.port ? ['-p', opts.port.toString()] : []),
`${opts.username}@ssh.${opts.proxyUrl}`,
opts.command,
];
}
}

View File

@ -136,8 +136,7 @@ export default class TunnelCmd extends Command {
// Ascertain device uuid
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
const uuid = await getOnlineTargetDeviceUuid(sdk, params.deviceOrFleet);
const device = await sdk.models.device.get(uuid);
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
logger.logInfo(`Opening a tunnel to ${uuid}...`);
const _ = await import('lodash');
const localListeners = _.chain(options.port)
@ -147,11 +146,7 @@ export default class TunnelCmd extends Command {
.map(async ({ localPort, localAddress, remotePort }) => {
try {
const { tunnelConnectionToDevice } = await import('../utils/tunnel');
const handler = await tunnelConnectionToDevice(
device.uuid,
remotePort,
sdk,
);
const handler = await tunnelConnectionToDevice(uuid, remotePort, sdk);
const { createServer } = await import('net');
const server = createServer(async (client: Socket) => {
@ -162,7 +157,7 @@ export default class TunnelCmd extends Command {
client.remotePort || 0,
client.localAddress,
client.localPort,
device.vpn_address || '',
uuid,
remotePort,
);
} catch (err) {
@ -171,7 +166,7 @@ export default class TunnelCmd extends Command {
client.remotePort || 0,
client.localAddress,
client.localPort,
device.vpn_address || '',
uuid,
remotePort,
err,
);
@ -186,15 +181,15 @@ export default class TunnelCmd extends Command {
});
logger.logInfo(
` - tunnelling ${localAddress}:${localPort} to ${device.uuid}:${remotePort}`,
` - tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}`,
);
return true;
} catch (err) {
logger.logWarn(
` - not tunnelling ${localAddress}:${localPort} to ${
device.uuid
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
` - not tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}, failed ${JSON.stringify(
err.message,
)}`,
);
return false;

View File

@ -16,12 +16,7 @@
*/
import * as packageJSON from '../package.json';
import { getBalenaSdk, stripIndent } from './utils/lazy';
interface CachedUsername {
token: string;
username: string;
}
import { stripIndent } from './utils/lazy';
/**
* Track balena CLI usage events (product improvement analytics).
@ -49,40 +44,13 @@ export async function trackCommand(commandSignature: string) {
scope.setExtra('command', commandSignature);
});
}
const settings = await import('balena-settings-client');
const username = await (async () => {
const getStorage = await import('balena-settings-storage');
const dataDirectory = settings.get<string>('dataDirectory');
const storage = getStorage({ dataDirectory });
let token;
try {
token = await storage.get('token');
} catch {
// If we can't get a token then we can't get a username
return;
}
try {
const result = (await storage.get('cachedUsername')) as CachedUsername;
if (result.token === token) {
return result.username;
}
} catch {
// ignore
}
try {
const balena = getBalenaSdk();
const $username = await balena.auth.whoami();
await storage.set('cachedUsername', {
token,
username: $username,
} as CachedUsername);
return $username;
} catch {
return;
}
})();
const { getCachedUsername } = await import('./utils/bootstrap');
let username: string | undefined;
try {
username = (await getCachedUsername())?.username;
} catch {
// ignore
}
if (!process.env.BALENARC_NO_SENTRY) {
Sentry!.configureScope((scope) => {
scope.setUser({
@ -96,6 +64,7 @@ export async function trackCommand(commandSignature: string) {
!process.env.BALENA_CLI_TEST_TYPE &&
!process.env.BALENARC_NO_ANALYTICS
) {
const settings = await import('balena-settings-client');
const balenaUrl = settings.get<string>('balenaUrl');
await sendEvent(balenaUrl, `[CLI] ${commandSignature}`, username);
}

View File

@ -27,14 +27,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
// `@types/node` does not know about `options: { bigint?: boolean }`
type statT = (
fPath: string,
options: { bigint?: boolean },
) => fs.Stats | Promise<fs.Stats>;
// async stat does not work with pkg's internal `/snapshot` filesystem
const stat: statT = process.pkg ? fs.statSync : fs.promises.stat;
const stat = process.pkg ? fs.statSync : fs.promises.stat;
let fastBootStarted = false;

View File

@ -119,3 +119,61 @@ export async function pkgExec(modFunc: string, args: string[]) {
console.error(err);
}
}
export interface CachedUsername {
token: string;
username: string;
}
let cachedUsername: CachedUsername | undefined;
/**
* Return the parsed contents of the `~/.balena/cachedUsername` file. If the file
* does not exist, create it with the details from the cloud. If not connected
* to the internet, return undefined. This function is used by `lib/events.ts`
* (event tracking) and `lib/utils/device/ssh.ts` and needs to gracefully handle
* the scenario of not being connected to the internet.
*/
export async function getCachedUsername(): Promise<CachedUsername | undefined> {
if (cachedUsername) {
return cachedUsername;
}
const [{ getBalenaSdk }, getStorage, settings] = await Promise.all([
import('./lazy'),
import('balena-settings-storage'),
import('balena-settings-client'),
]);
const dataDirectory = settings.get<string>('dataDirectory');
const storage = getStorage({ dataDirectory });
let token: string | undefined;
try {
token = (await storage.get('token')) as string | undefined;
} catch {
// ignore
}
if (!token) {
// If we can't get a token then we can't get a username
return;
}
try {
const result = (await storage.get('cachedUsername')) as
| CachedUsername
| undefined;
if (result && result.token === token && result.username) {
cachedUsername = result;
return cachedUsername;
}
} catch {
// ignore
}
try {
const username = await getBalenaSdk().auth.whoami();
if (username) {
cachedUsername = { token, username };
await storage.set('cachedUsername', cachedUsername);
}
} catch {
// ignore (not connected to the internet?)
}
return cachedUsername;
}

View File

@ -97,14 +97,18 @@ export const deviceType = flags.string({
required: true,
});
export const deviceTypeIgnored = isV14()
? undefined
: flags.string({
description: 'ignored - no longer required',
char: 't',
required: false,
hidden: true,
});
export const deviceTypeIgnored = {
...(isV14()
? {}
: {
type: flags.string({
description: 'ignored - no longer required',
char: 't',
required: false,
hidden: true,
}),
}),
};
export const json: IBooleanFlag<boolean> = flags.boolean({
char: 'j',

View File

@ -61,6 +61,7 @@ export interface ComposeOpts {
export interface ComposeCliFlags {
emulated: boolean;
dockerfile?: string;
dockercompose?: string;
logs: boolean;
nologs: boolean;
'multi-dockerignore': boolean;

View File

@ -28,6 +28,7 @@ import type {
ImageDescriptor,
} from 'resin-compose-parse';
import type * as MultiBuild from 'resin-multibuild';
import * as semver from 'semver';
import type { Duplex, Readable } from 'stream';
import type { Pack } from 'tar-stream';
@ -1348,9 +1349,6 @@ async function pushServiceImages(
);
}
// TODO: This should be shared between the CLI & the Builder
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
export async function deployProject(
docker: Dockerode,
logger: Logger,
@ -1373,10 +1371,10 @@ export async function deployProject(
const contractPath = path.join(projectPath, 'balena.yml');
const contract = await getContractContent(contractPath);
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
if (contract?.version && !semver.valid(contract.version)) {
throw new ExpectedError(stripIndent`\
Error: expected the version field in "${contractPath}"
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
Error: the version field in "${contractPath}"
is not a valid semver`);
}
const $release = await runSpinner(
@ -1641,6 +1639,7 @@ function truncateString(str: string, len: number): string {
return str.slice(0, str.lastIndexOf('\n'));
}
// TODO: docker-compose naming
export const composeCliFlags: flags.Input<ComposeCliFlags> = {
emulated: flags.boolean({
description:
@ -1651,6 +1650,10 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
description:
'Alternative Dockerfile name/path, relative to the source folder',
}),
dockercompose: flags.string({
description:
'Alternative docker-compose.yml name in the source root folder',
}),
logs: flags.boolean({
description:
'No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.',

View File

@ -13,89 +13,140 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type { ContainerInfo } from 'dockerode';
import { ExpectedError } from '../../errors';
import { stripIndent } from '../lazy';
export interface DeviceSSHOpts {
address: string;
port?: number;
import {
findBestUsernameForDevice,
getRemoteCommandOutput,
runRemoteCommand,
SshRemoteCommandOpts,
} from '../ssh';
export interface DeviceSSHOpts extends SshRemoteCommandOpts {
forceTTY?: boolean;
verbose: boolean;
service?: string;
}
export const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
/**
* List the running containers on the device over ssh, and return the full
* container name that matches the given service name.
*
* Note: In the past, two other approaches were implemented for this function:
*
* - Obtaining container IDs through a supervisor API call:
* '/supervisor/v2/containerId' endpoint, via cloud.
* - Obtaining container IDs using 'dockerode' connected directly to
* balenaEngine on a device, TCP port 2375.
*
* The problem with using the supervisor API is that it means that 'balena ssh'
* becomes dependent on the supervisor being up an running, but sometimes ssh
* is needed to investigate devices issues where the supervisor has got into
* trouble (e.g. supervisor in restart loop). This is the subject of CLI issue
* https://github.com/balena-io/balena-cli/issues/1560 .
*
* The problem with using dockerode to connect directly to port 2375 (balenaEngine)
* is that it only works with development variants of balenaOS. Production variants
* block access to port 2375 for security reasons. 'balena ssh' should support
* production variants as well, especially after balenaOS v2.44.0 that introduced
* support for using the cloud account username for ssh authentication.
*
* Overall, the most reliable approach is to run 'balena-engine ps' over ssh.
* It is OK to depend on balenaEngine because ssh to a container is implemented
* through 'balena-engine exec' anyway, and of course it is OK to depend on ssh
* itself.
*/
export async function getContainerIdForService(
opts: SshRemoteCommandOpts & { service: string; deviceUuid?: string },
): Promise<string> {
opts.cmd = `"${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`;
if (opts.deviceUuid) {
// If a device UUID is given, perform ssh via cloud proxy 'host' command
opts.cmd = `host ${opts.deviceUuid} ${opts.cmd}`;
}
const psLines: string[] = (
await getRemoteCommandOutput({ ...opts, stderr: 'inherit' })
).stdout
.toString()
.split('\n')
.filter((l) => l);
const { escapeRegExp } = await import('lodash');
const regex = new RegExp(`(?:^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
// Old balenaOS container name pattern:
// main_1234567_2345678
// New balenaOS container name patterns:
// main_1234567_2345678_a000b111c222d333e444f555a666b777
// main_1_1_localrelease
const nameRegex = /(?:^|\/)([a-zA-Z0-9_-]+)_\d+_\d+(?:_.+)?$/;
const serviceNames: string[] = [];
const containerNames: string[] = [];
let containerId: string | undefined;
// sample psLine: 'b603c74e951e bar_4587562_2078151_3261c9d4c22f2c53a5267be459c89990'
for (const psLine of psLines) {
const [cId, name] = psLine.split(' ');
if (cId && name) {
if (regex.test(name)) {
containerNames.push(name);
containerId = cId;
}
const match = name.match(nameRegex);
if (match) {
serviceNames.push(match[1]);
}
}
}
if (containerNames.length > 1) {
const [s, d] = [opts.service, opts.deviceUuid || opts.hostname];
throw new ExpectedError(stripIndent`
Found more than one container matching service name "${s}" on device "${d}":
${containerNames.join(', ')}
Use different service names to avoid ambiguity.
`);
}
if (!containerId) {
const [s, d] = [opts.service, opts.deviceUuid || opts.hostname];
throw new ExpectedError(
`Could not find a container matching service name "${s}" on device "${d}".${
serviceNames.length > 0
? `\nAvailable services:\n\t${serviceNames.join('\n\t')}`
: ''
}`,
);
}
return containerId;
}
export async function performLocalDeviceSSH(
opts: DeviceSSHOpts,
): Promise<void> {
const { escapeRegExp, reduce } = await import('lodash');
const { spawnSshAndThrowOnError } = await import('../ssh');
const { ExpectedError } = await import('../../errors');
// Before we started using `findBestUsernameForDevice`, we tried the approach
// of attempting ssh with the 'root' username first and, if that failed, then
// attempting ssh with a regular user (balenaCloud username). The problem with
// that approach was that it would print the following message to the console:
// "root@192.168.1.36: Permission denied (publickey)"
// ... right before having success as a regular user, which looked broken or
// confusing from users' point of view. Capturing stderr to prevent that
// message from being printed is tricky because the messages printed to stderr
// may include the stderr output of remote commands that are of interest to
// the user.
const username = await findBestUsernameForDevice(opts.hostname, opts.port);
let cmd = '';
let command = '';
if (opts.service != null) {
// Get the containers which are on-device. Currently we
// are single application, which means we can assume any
// container which fulfills the form of
// $serviceName_$appId_$releaseId is what we want. Once
// we have multi-app, we should show a dialog which
// allows the user to choose the correct container
const Docker = await import('dockerode');
const docker = new Docker({
host: opts.address,
port: 2375,
if (opts.service) {
const containerId = await getContainerIdForService({
...opts,
service: opts.service,
username,
});
const regex = new RegExp(`(^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
const nameRegex = /\/?([a-zA-Z0-9_-]+)_\d+_\d+/;
let allContainers: ContainerInfo[];
try {
allContainers = await docker.listContainers();
} catch (_e) {
throw new ExpectedError(stripIndent`
Could not access docker daemon on device ${opts.address}.
Please ensure the device is in local mode.`);
}
const serviceNames: string[] = [];
const containers: Array<{ id: string; name: string }> = [];
for (const container of allContainers) {
for (const name of container.Names) {
if (regex.test(name)) {
containers.push({ id: container.Id, name });
break;
}
const match = name.match(nameRegex);
if (match) {
serviceNames.push(match[1]);
}
}
}
if (containers.length === 0) {
throw new ExpectedError(
`Could not find a service on device with name ${opts.service}. ${
serviceNames.length > 0
? `Available services:\n${reduce(
serviceNames,
(str, name) => `${str}\t${name}\n`,
'',
)}`
: ''
}`,
);
}
if (containers.length > 1) {
throw new ExpectedError(stripIndent`
Found more than one container matching service name "${opts.service}":
${containers.map((container) => container.name).join(', ')}
Use different service names to avoid ambiguity.
`);
}
const containerId = containers[0].id;
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
// stdin (fd=0) is not a tty when data is piped in, for example
// echo 'ls -la; exit;' | balena ssh 192.168.0.20 service1
@ -103,17 +154,8 @@ export async function performLocalDeviceSSH(
// https://assets.balena.io/newsletter/2020-01/pipe.png
const isTTY = !!opts.forceTTY || (await import('tty')).isatty(0);
const ttyFlag = isTTY ? '-t' : '';
command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
cmd = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
}
return spawnSshAndThrowOnError([
...(opts.verbose ? ['-vvv'] : []),
'-t',
...['-p', opts.port ? opts.port.toString() : '22222'],
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
`root@${opts.address}`,
...(command ? [command] : []),
]);
await runRemoteCommand({ ...opts, cmd, username });
}

View File

@ -174,14 +174,8 @@ export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
);
}
export interface ExtendedDockerOptions extends dockerode.DockerOptions {
docker?: string; // socket path, e.g. /var/run/docker.sock
dockerHost?: string; // host name or IP address
dockerPort?: number; // TCP port number, e.g. 2375
}
export async function getDocker(
options: ExtendedDockerOptions,
options: DockerConnectionCliFlags,
): Promise<dockerode> {
const connectOpts = await generateConnectOpts(options);
const client = await createClient(connectOpts);
@ -196,14 +190,18 @@ export async function createClient(
return new Docker(opts);
}
async function generateConnectOpts(opts: ExtendedDockerOptions) {
let connectOpts: dockerode.DockerOptions = {};
// Start with docker-modem defaults which take several env vars into account,
// including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
const Modem = require('docker-modem');
const defaultOpts = new Modem();
/**
* Initialize Docker connection options with the default values from the
* 'docker-modem' package, which takes several env vars into account,
* including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
* https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
*
* @param opts Command line options like --dockerHost and --dockerPort
*/
export function getDefaultDockerModemOpts(
opts: DockerConnectionCliFlags,
): dockerode.DockerOptions {
const connectOpts: dockerode.DockerOptions = {};
const optsOfInterest: Array<keyof dockerode.DockerOptions> = [
'ca',
'cert',
@ -215,9 +213,33 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
'username',
'timeout',
];
for (const opt of optsOfInterest) {
connectOpts[opt] = defaultOpts[opt];
const Modem = require('docker-modem');
const originalDockerHost = process.env.DOCKER_HOST;
try {
if (opts.dockerHost) {
process.env.DOCKER_HOST ||= opts.dockerPort
? `${opts.dockerHost}:${opts.dockerPort}`
: opts.dockerHost;
}
const defaultOpts = new Modem();
for (const opt of optsOfInterest) {
connectOpts[opt] = defaultOpts[opt];
}
} finally {
// Did you know? Any value assigned to `process.env.XXX` becomes a string.
// For example, `process.env.DOCKER_HOST = undefined` results in
// value 'undefined' (a 9-character string) being assigned.
if (originalDockerHost) {
process.env.DOCKER_HOST = originalDockerHost;
} else {
delete process.env.DOCKER_HOST;
}
}
return connectOpts;
}
export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
let connectOpts = getDefaultDockerModemOpts(opts);
// Now override the default options with any explicit command line options
if (opts.docker != null && opts.dockerHost == null) {
@ -241,9 +263,9 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
// These should be file paths (strings)
const tlsOpts = [opts.ca, opts.cert, opts.key];
// If any are set...
// If any tlsOpts are set...
if (tlsOpts.some((opt) => opt)) {
// but not all ()
// but not all
if (!tlsOpts.every((opt) => opt)) {
throw new ExpectedError(
'You must provide a CA, certificate and key in order to use TLS',
@ -258,7 +280,11 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
const [ca, cert, key] = await Promise.all(
tlsOpts.map((opt: string) => fs.readFile(opt, 'utf8')),
);
connectOpts = { ...connectOpts, ca, cert, key };
// Also ensure that the protocol is 'https' like 'docker-modem' does:
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L101-L103
// TODO: delete redundant logic from this function now that similar logic
// exists in the 'docker-modem' package.
connectOpts = { ...connectOpts, ca, cert, key, protocol: 'https' };
}
return connectOpts;

View File

@ -92,6 +92,7 @@ async function readDockerIgnoreFile(projectDir: string): Promise<string> {
return dockerIgnoreStr;
}
// TODO: docker-compose naming
/**
* Create an instance of '@balena/dockerignore', initialized with the contents
* of a .dockerignore file (if any) found at the given directory argument, plus

View File

@ -86,6 +86,7 @@ If the --registry-secrets option is not specified, and a secrets.yml or
secrets.json file exists in the balena directory (usually $HOME/.balena),
this file will be used instead.`;
// TODO: docker-compose naming
export const dockerignoreHelp =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at

View File

@ -207,35 +207,6 @@ export async function selectOrganization(organizations?: Organization[]) {
});
}
export async function awaitDevice(uuid: string) {
const balena = getBalenaSdk();
const deviceName = await balena.models.device.getName(uuid);
const visuals = getVisuals();
const spinner = new visuals.Spinner(
`Waiting for ${deviceName} to come online`,
);
const poll = async (): Promise<void> => {
const isOnline = await balena.models.device.isOnline(uuid);
if (isOnline) {
spinner.stop();
console.info(`The device **${deviceName}** is online!`);
return;
} else {
// Spinner implementation is smart enough to
// not start again if it was already started
spinner.start();
await delay(3000);
await poll();
}
};
console.info(`Waiting for ${deviceName} to connect to balena...`);
await poll();
return uuid;
}
export async function awaitDeviceOsUpdate(
uuid: string,
targetOsVersion: string,

View File

@ -20,7 +20,7 @@ import { ExpectedError, printErrorMessage } from '../errors';
import { getVisuals, stripIndent, getCliForm } from './lazy';
import Logger = require('./logger');
import { confirm } from './patterns';
import { exec, execBuffered, getDeviceOsRelease } from './ssh';
import { getLocalDeviceCmdStdout, getDeviceOsRelease } from './ssh';
const MIN_BALENAOS_VERSION = 'v2.14.0';
@ -80,7 +80,12 @@ export async function leave(
logger.logDebug('Deconfiguring...');
await deconfigure(deviceHostnameOrIp);
logger.logSuccess('Device successfully left the platform.');
logger.logSuccess(stripIndent`
Device successfully left the platform. The device will still be listed as part
of the fleet, but changes to the fleet will no longer affect the device and its
status will eventually be reported as 'Offline'. To irrecoverably delete the
device from the fleet, use the 'balena device rm' command or delete it through
the balenaCloud web dashboard.`);
}
async function execCommand(
@ -88,20 +93,25 @@ async function execCommand(
cmd: string,
msg: string,
): Promise<void> {
const through = await import('through2');
const { Writable } = await import('stream');
const visuals = getVisuals();
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
const innerSpinner = spinner.spinner;
const stream = through(function (data, _enc, cb) {
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
cb(null, data);
const stream = new Writable({
write(_chunk: Buffer, _enc, callback) {
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
callback();
},
});
spinner.start();
await exec(deviceIp, cmd, stream);
spinner.stop();
try {
await getLocalDeviceCmdStdout(deviceIp, cmd, stream);
} finally {
spinner.stop();
}
}
async function configure(deviceIp: string, config: any): Promise<void> {
@ -121,7 +131,7 @@ async function deconfigure(deviceIp: string): Promise<void> {
async function assertDeviceIsCompatible(deviceIp: string): Promise<void> {
const cmd = 'os-config --version';
try {
await execBuffered(deviceIp, cmd);
await getLocalDeviceCmdStdout(deviceIp, cmd);
} catch (err) {
if (err instanceof ExpectedError) {
throw err;

View File

@ -212,7 +212,7 @@ function handleBuilderMetadata(obj: BuilderMessage, build: RemoteBuild) {
}
const value = match[1];
const amount = match[2] || 1;
const amount = Number(match[2]) || 1;
switch (value) {
case 'erase':

View File

@ -16,147 +16,314 @@
*/
import { spawn, StdioOptions } from 'child_process';
import * as _ from 'lodash';
import { TypedError } from 'typed-error';
import { ExpectedError } from '../errors';
export class ExecError extends TypedError {
public cmd: string;
public exitCode: number;
export class SshPermissionDeniedError extends ExpectedError {}
constructor(cmd: string, exitCode: number) {
super(`Command '${cmd}' failed with error: ${exitCode}`);
export class RemoteCommandError extends ExpectedError {
cmd: string;
exitCode?: number;
exitSignal?: NodeJS.Signals;
constructor(cmd: string, exitCode?: number, exitSignal?: NodeJS.Signals) {
super(sshErrorMessage(cmd, exitSignal, exitCode));
this.cmd = cmd;
this.exitCode = exitCode;
this.exitSignal = exitSignal;
}
}
export async function exec(
deviceIp: string,
cmd: string,
stdout?: NodeJS.WritableStream,
): Promise<void> {
export interface SshRemoteCommandOpts {
cmd?: string;
hostname: string;
ignoreStdin?: boolean;
port?: number | 'cloud' | 'local';
proxyCommand?: string[];
username?: string;
verbose?: boolean;
}
export const stdioIgnore: {
stdin: 'ignore';
stdout: 'ignore';
stderr: 'ignore';
} = {
stdin: 'ignore',
stdout: 'ignore',
stderr: 'ignore',
};
export function sshArgsForRemoteCommand({
cmd = '',
hostname,
ignoreStdin = false,
port,
proxyCommand,
username = 'root',
verbose = false,
}: SshRemoteCommandOpts): string[] {
port = port === 'local' ? 22222 : port === 'cloud' ? 22 : port;
return [
...(verbose ? ['-vvv'] : []),
...(ignoreStdin ? ['-n'] : []),
'-t',
...(port ? ['-p', port.toString()] : []),
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
...(proxyCommand && proxyCommand.length
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
: []),
`${username}@${hostname}`,
...(cmd ? [cmd] : []),
];
}
/**
* Execute the given command on a local balenaOS device over ssh.
* @param cmd Shell command to execute on the device
* @param hostname Device's hostname or IP address
* @param port SSH server TCP port number or 'local' (22222) or 'cloud' (22)
* @param stdin Readable stream to pipe to the remote command stdin,
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
* @param stdout Writeable stream to pipe from the remote command stdout,
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
* @param stderr Writeable stream to pipe from the remote command stdout,
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
* @param username SSH username for authorization. With balenaOS 2.44.0 or
* later, it can be a balenaCloud username.
* @param verbose Produce debugging output
*/
export async function runRemoteCommand({
cmd = '',
hostname,
port,
proxyCommand,
stdin = 'inherit',
stdout = 'inherit',
stderr = 'inherit',
username = 'root',
verbose = false,
}: SshRemoteCommandOpts & {
stdin?: 'ignore' | 'inherit' | NodeJS.ReadableStream;
stdout?: 'ignore' | 'inherit' | NodeJS.WritableStream;
stderr?: 'ignore' | 'inherit' | NodeJS.WritableStream;
}): Promise<void> {
let ignoreStdin: boolean;
if (stdin === 'ignore') {
// Set ignoreStdin=true in order for the "ssh -n" option to be used to
// prevent the ssh client from using the CLI process stdin. In addition,
// stdin must be forced to 'inherit' (if it is not a readable stream) in
// order to work around a bug in older versions of the built-in Windows
// 10 ssh client that otherwise prints the following to stderr and
// hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
// They actually fixed the bug in newer versions of the ssh client:
// https://github.com/PowerShell/Win32-OpenSSH/issues/856 but users
// have to manually download and install a new client.
ignoreStdin = true;
stdin = 'inherit';
} else {
ignoreStdin = false;
}
const { which } = await import('./which');
const program = await which('ssh');
const args = [
'-n',
'-t',
'-p',
'22222',
'-o',
'LogLevel=ERROR',
'-o',
'StrictHostKeyChecking=no',
'-o',
'UserKnownHostsFile=/dev/null',
`root@${deviceIp}`,
const args = sshArgsForRemoteCommand({
cmd,
];
hostname,
ignoreStdin,
port,
proxyCommand,
username,
verbose,
});
if (process.env.DEBUG) {
const logger = (await import('./logger')).getLogger();
logger.logDebug(`Executing [${program},${args}]`);
}
// Note: stdin must be 'inherit' to workaround a bug in older versions of
// the built-in Windows 10 ssh client that otherwise prints the following
// to stderr and hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
// They fixed the bug in newer versions of the ssh client:
// https://github.com/PowerShell/Win32-OpenSSH/issues/856
// but users whould have to manually download and install a new client.
// Note that "ssh -n" does not solve the problem, but should in theory
// prevent the ssh client from using the CLI process stdin, even if it
// is connected with 'inherit'.
const stdio: StdioOptions = [
'inherit',
stdout ? 'pipe' : 'inherit',
'inherit',
typeof stdin === 'string' ? stdin : 'pipe',
typeof stdout === 'string' ? stdout : 'pipe',
typeof stderr === 'string' ? stderr : 'pipe',
];
let exitCode: number | undefined;
let exitSignal: NodeJS.Signals | undefined;
try {
[exitCode, exitSignal] = await new Promise<[number, NodeJS.Signals]>(
(resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', (code, signal) => resolve([code, signal]));
const exitCode = await new Promise<number>((resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', resolve);
if (stdout) {
ps.stdout.pipe(stdout);
}
});
if (exitCode !== 0) {
throw new ExecError(cmd, exitCode);
if (ps.stdin && stdin && typeof stdin !== 'string') {
stdin.pipe(ps.stdin);
}
if (ps.stdout && stdout && typeof stdout !== 'string') {
ps.stdout.pipe(stdout);
}
if (ps.stderr && stderr && typeof stderr !== 'string') {
ps.stderr.pipe(stderr);
}
},
);
} catch (error) {
const msg = [
`ssh failed with exit code=${exitCode} signal=${exitSignal}:`,
`[${program}, ${args.join(', ')}]`,
...(error ? [`${error}`] : []),
];
throw new ExpectedError(msg.join('\n'));
}
if (exitCode || exitSignal) {
throw new RemoteCommandError(cmd, exitCode, exitSignal);
}
}
export async function execBuffered(
deviceIp: string,
cmd: string,
enc?: string,
): Promise<string> {
const through = await import('through2');
const buffer: string[] = [];
await exec(
deviceIp,
/**
* Execute the given command on a local balenaOS device over ssh.
* Capture stdout and/or stderr to Buffers and return them.
*
* @param deviceIp IP address of the local device
* @param cmd Shell command to execute on the device
* @param opts Options
* @param opts.username SSH username for authorization. With balenaOS 2.44.0 or
* later, it may be a balenaCloud username. Otherwise, 'root'.
* @param opts.stdin Passed through to the runRemoteCommand function
* @param opts.stdout If 'capture', capture stdout to a Buffer.
* @param opts.stderr If 'capture', capture stdout to a Buffer.
*/
export async function getRemoteCommandOutput({
cmd,
hostname,
port,
proxyCommand,
stdin = 'ignore',
stdout = 'capture',
stderr = 'capture',
username = 'root',
verbose = false,
}: SshRemoteCommandOpts & {
stdin?: 'ignore' | 'inherit' | NodeJS.ReadableStream;
stdout?: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream;
stderr?: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream;
}): Promise<{ stdout: Buffer; stderr: Buffer }> {
const { Writable } = await import('stream');
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];
const stdoutStream = new Writable({
write(chunk: Buffer, _enc, callback) {
stdoutChunks.push(chunk);
callback();
},
});
const stderrStream = new Writable({
write(chunk: Buffer, _enc, callback) {
stderrChunks.push(chunk);
callback();
},
});
await runRemoteCommand({
cmd,
through(function (data, _enc, cb) {
buffer.push(data.toString(enc));
cb();
}),
);
return buffer.join('');
hostname,
port,
proxyCommand,
stdin,
stdout: stdout === 'capture' ? stdoutStream : stdout,
stderr: stderr === 'capture' ? stderrStream : stderr,
username,
verbose,
});
return {
stdout: Buffer.concat(stdoutChunks),
stderr: Buffer.concat(stderrChunks),
};
}
/** Convenience wrapper for getRemoteCommandOutput */
export async function getLocalDeviceCmdStdout(
hostname: string,
cmd: string,
stdout: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream = 'capture',
): Promise<Buffer> {
const port = 'local';
return (
await getRemoteCommandOutput({
cmd,
hostname,
port,
stdout,
stderr: 'inherit',
username: await findBestUsernameForDevice(hostname, port),
})
).stdout;
}
/**
* Run a trivial 'exit 0' command over ssh on the target hostname (typically the
* IP address of a local device) with the 'root' username, in order to determine
* whether root authentication suceeds. It should succeed with development
* variants of balenaOS and fail with production variants, unless a ssh key was
* added to the device's 'config.json' file.
* @return True if succesful, false on any errors.
*/
export const isRootUserGood = _.memoize(async (hostname: string, port) => {
try {
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...stdioIgnore });
} catch (e) {
return false;
}
return true;
});
/**
* Determine whether the given local device (hostname or IP address) should be
* accessed as the 'root' user or as a regular cloud user (balenaCloud or
* openBalena). Where possible, the root user is preferable because:
* - It allows ssh to be used in air-gapped scenarios (no internet access).
* Logging in as a regular user requires the device to fetch public keys from
* the cloud backend.
* - Root authentication is significantly faster for local devices (a fraction
* of a second versus 5+ seconds).
* - Non-root authentication requires balenaOS v2.44.0 or later, so not (yet)
* universally possible.
*/
export const findBestUsernameForDevice = _.memoize(
async (hostname: string, port): Promise<string> => {
let username: string | undefined;
if (await isRootUserGood(hostname, port)) {
username = 'root';
} else {
const { getCachedUsername } = await import('./bootstrap');
username = (await getCachedUsername())?.username;
}
if (!username) {
const { stripIndent } = await import('./lazy');
throw new ExpectedError(stripIndent`
SSH authentication failed for 'root@${hostname}'.
Please login with 'balena login' for alternative authentication.`);
}
return username;
},
);
/**
* Return a device's balenaOS release by executing 'cat /etc/os-release'
* over ssh to the given deviceIp address. The result is cached with
* lodash's memoize.
*/
export const getDeviceOsRelease = _.memoize(async (deviceIp: string) =>
execBuffered(deviceIp, 'cat /etc/os-release'),
export const getDeviceOsRelease = _.memoize(async (hostname: string) =>
(await getLocalDeviceCmdStdout(hostname, 'cat /etc/os-release')).toString(),
);
// TODO: consolidate the various forms of executing ssh child processes
// in the CLI, like exec and spawn, starting with the files:
// lib/actions/ssh.ts
// lib/utils/ssh.ts
// lib/utils/device/ssh.ts
/**
* Obtain the full path for ssh using which, then spawn a child process.
* - If the child process returns error code 0, return the function normally
* (do not throw an error).
* - If the child process returns a non-zero error code, set process.exitCode
* to that error code, and throw ExpectedError with a warning message.
* - If the child process is terminated by a process signal, set
* process.exitCode = 1, and throw ExpectedError with a warning message.
*/
export async function spawnSshAndThrowOnError(
args: string[],
options?: import('child_process').SpawnOptions,
) {
const { whichSpawn } = await import('./which');
const [exitCode, exitSignal] = await whichSpawn(
'ssh',
args,
options,
true, // returnExitCodeOrSignal
);
if (exitCode || exitSignal) {
// ssh returns a wide range of exit codes, including return codes of
// interactive shells. For example, if the user types CTRL-C on an
// interactive shell and then `exit`, ssh returns error code 130.
// Another example, typing "exit 1" on an interactive shell causes ssh
// to return exit code 1. In these cases, print a short one-line warning
// message, and exits the CLI process with the same error code.
process.exitCode = exitCode;
throw new ExpectedError(sshErrorMessage(exitSignal, exitCode));
}
}
function sshErrorMessage(exitSignal?: string, exitCode?: number) {
function sshErrorMessage(cmd: string, exitSignal?: string, exitCode?: number) {
const msg: string[] = [];
cmd = cmd ? `Remote command "${cmd}"` : 'Process';
if (exitSignal) {
msg.push(`Warning: ssh process was terminated with signal "${exitSignal}"`);
msg.push(`SSH: ${cmd} terminated with signal "${exitSignal}"`);
} else {
msg.push(`Warning: ssh process exited with non-zero code "${exitCode}"`);
msg.push(`SSH: ${cmd} exited with non-zero status code "${exitCode}"`);
switch (exitCode) {
case 255:
msg.push(`

View File

@ -98,7 +98,7 @@ async function spawnAndPipe(
resolve();
}
});
if (stderr) {
if (stderr && ps.stderr) {
ps.stderr.pipe(stderr);
}
});

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2019 Balena
Copyright 2016-2022 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -40,14 +40,35 @@ export function notify() {
}
}
const up = notifier.update;
if (
up &&
(require('semver') as typeof import('semver')).lt(up.current, up.latest)
) {
notifier.notify({
defer: false,
message: `Update available ${up.current}${up.latest}\n
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md`,
});
const message = up && getNotifierMessage(up);
if (message) {
notifier.notify({ defer: false, message });
}
}
export function getNotifierMessage(updateInfo: UpdateNotifier.UpdateInfo) {
const semver = require('semver') as typeof import('semver');
const message: string[] = [];
const [current, latest] = [updateInfo.current, updateInfo.latest];
if (semver.lt(current, latest)) {
message.push(
`Update available ${current}${latest}`,
'https://github.com/balena-io/balena-cli/blob/master/INSTALL.md',
);
const currentMajor = semver.major(current);
const latestMajor = semver.major(latest);
if (currentMajor !== latestMajor) {
message.push(
'',
`Check the v${latestMajor} release notes at:`,
getReleaseNotesUrl(latestMajor),
);
}
}
return message.join('\n');
}
function getReleaseNotesUrl(majorVersion: number) {
return `https://github.com/balena-io/balena-cli/wiki/CLI-v${majorVersion}-Release-Notes`;
}

View File

@ -95,52 +95,3 @@ export async function which(
}
return programPath;
}
/**
* Call which(programName) and spawn() with the given arguments.
*
* If returnExitCodeOrSignal is true, the returned promise will resolve to
* an array [code, signal] with the child process exit code number or exit
* signal string respectively (as provided by the spawn close event).
*
* If returnExitCodeOrSignal is false, the returned promise will reject with
* a custom error if the child process returns a non-zero exit code or a
* non-empty signal string (as reported by the spawn close event).
*
* In either case and if spawn itself emits an error event or fails synchronously,
* the returned promise will reject with a custom error that includes the error
* message of spawn's error.
*/
export async function whichSpawn(
programName: string,
args: string[],
options: import('child_process').SpawnOptions = { stdio: 'inherit' },
returnExitCodeOrSignal = false,
): Promise<[number | undefined, string | undefined]> {
const { spawn } = await import('child_process');
const program = await which(programName);
if (process.env.DEBUG) {
console.error(`[debug] [${program}, ${args.join(', ')}]`);
}
let error: Error | undefined;
let exitCode: number | undefined;
let exitSignal: string | undefined;
try {
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
spawn(program, args, options)
.on('error', reject)
.on('close', (code, signal) => resolve([code, signal]));
});
} catch (err) {
error = err;
}
if (error || (!returnExitCodeOrSignal && (exitCode || exitSignal))) {
const msg = [
`${programName} failed with exit code=${exitCode} signal=${exitSignal}:`,
`[${program}, ${args.join(', ')}]`,
...(error ? [`${error}`] : []),
];
throw new Error(msg.join('\n'));
}
return [exitCode, exitSignal];
}

516
npm-shrinkwrap.json generated
View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "13.1.6",
"version": "13.4.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1093,15 +1093,6 @@
"integrity": "sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==",
"dev": true
},
"@babel/runtime": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
"integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"@babel/template": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz",
@ -1330,14 +1321,6 @@
"tslint-no-unused-expression-chai": "^0.1.4",
"typescript": "^4.2.4",
"yargs": "^16.2.0"
},
"dependencies": {
"@types/node": {
"version": "12.20.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.37.tgz",
"integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA==",
"dev": true
}
}
},
"@balena/node-beaglebone-usbboot": {
@ -2574,9 +2557,9 @@
}
},
"@types/dockerode": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz",
"integrity": "sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.8.tgz",
"integrity": "sha512-/Hip29GzPBWfbSS87lyQDVoB7Ja+kr8oOFWXsySxNFa7jlyj3Yws8LaZRmn1xZl7uJH3Xxsg0oI09GHpT1pIBw==",
"dev": true,
"requires": {
"@types/docker-modem": "*",
@ -2849,9 +2832,9 @@
}
},
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
"version": "12.20.42",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.42.tgz",
"integrity": "sha512-aI3/oo5DzyiI5R/xAhxxRzfZlWlsbbqdgxfTPkqu/Zt+23GXiJvMCyPJT4+xKSXOnLqoL8jJYMLTwvK2M3a5hw=="
},
"@types/node-cleanup": {
"version": "2.1.2",
@ -2963,12 +2946,9 @@
}
},
"@types/semver": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.1.tgz",
"integrity": "sha512-ooD/FJ8EuwlDKOI6D9HWxgIgJjMg2cuziXm/42npDC8y4NjxplBUn9loewZiBNCt44450lHAU0OSb51/UqXeag==",
"requires": {
"@types/node": "*"
}
"version": "7.3.9",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ=="
},
"@types/serve-static": {
"version": "1.13.10",
@ -3006,9 +2986,9 @@
}
},
"@types/ssh2": {
"version": "0.5.49",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.49.tgz",
"integrity": "sha512-ffxhQhJqgTzrw8NxHTgkaDtAmAj2qxCyoves7ztpRgqvzbHcZTpTcm+ATWuuCbPQzxnnF4F3SGGTLGEWTZpwqA==",
"version": "0.5.52",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
"integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==",
"dev": true,
"requires": {
"@types/node": "*",
@ -3782,6 +3762,11 @@
"rimraf": "^3.0.2"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"balena-sdk": {
"version": "15.59.2",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.59.2.tgz",
@ -3827,12 +3812,12 @@
}
},
"balena-preload": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-11.0.0.tgz",
"integrity": "sha512-2LuPTw6LoVxuasGhn+cLyA5n7kjvwBtQmBsg08KNhcbEpWLkzrV5jhpNxlMPUuoC2xcMv5Rcg6FWbY87QV5zYQ==",
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-12.0.0.tgz",
"integrity": "sha512-BD4ayIqqopJB0KFFjjlz0rIpcbbHojG8El8qOBLJHvidatgtgVs5xFWBoF5B7fgdJdjRsclA/AbUMZwovN7t3w==",
"requires": {
"archiver": "^3.1.1",
"balena-sdk": "^15.44.0",
"balena-sdk": "^16.0.0",
"bluebird": "^3.7.2",
"compare-versions": "^3.6.0",
"docker-progress": "^5.0.0",
@ -3863,32 +3848,6 @@
"zip-stream": "^2.1.2"
}
},
"balena-sdk": {
"version": "15.59.2",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.59.2.tgz",
"integrity": "sha512-pMNwqqnqtets6PoYxRQhOHBnk7Say4gNhInAvS69UlEhraCR0Xau8rJk9G2IthWDA4tpR2In3u4SQJMIHYj84w==",
"requires": {
"@balena/es-version": "^1.0.0",
"@types/lodash": "^4.14.168",
"@types/memoizee": "^0.4.5",
"@types/node": "^10.17.55",
"abortcontroller-polyfill": "^1.7.1",
"balena-auth": "^4.1.0",
"balena-errors": "^4.7.1",
"balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0",
"balena-request": "^11.5.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"moment": "^2.29.1",
"ndjson": "^2.0.0",
"semver": "^7.3.4",
"tslib": "^2.1.0"
}
},
"compress-commons": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
@ -3960,11 +3919,6 @@
}
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"zip-stream": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz",
@ -4056,6 +4010,11 @@
"tslib": "^2.1.0"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"balena-hup-action-utils": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.1.0.tgz",
@ -4122,6 +4081,13 @@
"@types/node": "^10.17.26",
"balena-errors": "^4.7.1",
"tslib": "^2.0.0"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
}
}
},
"balena-sync": {
@ -4150,6 +4116,11 @@
"typed-error": "^2.0.0"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"balena-sdk": {
"version": "15.59.2",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.59.2.tgz",
@ -4299,9 +4270,9 @@
"dev": true
},
"big-integer": {
"version": "1.6.50",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.50.tgz",
"integrity": "sha512-+O2uoQWFRo8ysZNo/rjtri2jIwjr3XfeAgRjAUADRqGG+ZITvyn8J1kvXLTaKVr3hhGXk+f23tKfdzmklVM9vQ=="
"version": "1.6.51",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
},
"binary": {
"version": "0.3.0",
@ -4610,12 +4581,6 @@
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
"dev": true
},
"byline": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
"integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=",
"dev": true
},
"bytes": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
@ -6164,9 +6129,9 @@
"integrity": "sha512-djh3R7KXkEPm80PXK9xbz8bCfEFuU11Tmf5l9IXKdjBPx91/cOqhwOwtOq6s35B8TqrwY6L4xLphmyYmJT0ZXw=="
},
"docker-modem": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.2.tgz",
"integrity": "sha512-K6ahu0IaJXqRqiAUZYo01n/6MkHir1c5mVJx1//JpyRmePYoIOC7oPR2vSx8rCaxIt7qRc77v9ewxljl6Qatdg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.0.tgz",
"integrity": "sha512-WwFajJ8I5geZ/dDZ5FDMDA6TBkWa76xWwGIGw8uzUjNUGCN0to83wJ8Oi1AxrJTC0JBn+7fvIxUctnawtlwXeg==",
"requires": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
@ -6881,22 +6846,22 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"escodegen": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
"integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
"dev": true,
"requires": {
"esprima": "^4.0.1",
"estraverse": "^4.2.0",
"estraverse": "^5.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1",
"source-map": "~0.6.1"
},
"dependencies": {
"estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true
}
}
@ -9326,9 +9291,9 @@
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
},
"into-stream": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.1.tgz",
"integrity": "sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz",
"integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==",
"dev": true,
"requires": {
"from2": "^2.3.0",
@ -11553,13 +11518,26 @@
}
},
"multistream": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz",
"integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz",
"integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==",
"dev": true,
"requires": {
"inherits": "^2.0.1",
"readable-stream": "^2.0.5"
"once": "^1.4.0",
"readable-stream": "^3.6.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"mute-stream": {
@ -13215,51 +13193,74 @@
}
},
"pkg": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/pkg/-/pkg-4.5.1.tgz",
"integrity": "sha512-UXKL88jGQ+FD4//PyrFeRcqurVQ3BVIfUNaEU9cXY24EJz08JyBj85qrGh0CFGvyzNb1jpwHOnns5Sw0M5H92Q==",
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.5.1.tgz",
"integrity": "sha512-3IiUgwYRQBfXcmdBakjqttRrhpruZ1h/UCobtra2IN4S29eJhgxr39Dd8EZxUikgSLUH3v/eUWO3ZInSmlSXpw==",
"dev": true,
"requires": {
"@babel/parser": "7.13.12",
"@babel/runtime": "7.13.10",
"chalk": "^3.0.0",
"escodegen": "^1.14.1",
"fs-extra": "^8.1.0",
"globby": "^11.0.0",
"into-stream": "^5.1.1",
"@babel/parser": "7.16.2",
"@babel/types": "7.16.0",
"chalk": "^4.1.2",
"escodegen": "^2.0.0",
"fs-extra": "^9.1.0",
"globby": "^11.0.4",
"into-stream": "^6.0.0",
"minimist": "^1.2.5",
"multistream": "^2.1.1",
"pkg-fetch": "2.6.9",
"prebuild-install": "6.0.1",
"multistream": "^4.1.0",
"pkg-fetch": "3.2.5",
"prebuild-install": "6.1.4",
"progress": "^2.0.3",
"resolve": "^1.15.1",
"stream-meter": "^1.0.4"
"resolve": "^1.20.0",
"stream-meter": "^1.0.4",
"tslib": "2.3.1"
},
"dependencies": {
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"@babel/parser": {
"version": "7.16.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.2.tgz",
"integrity": "sha512-RUVpT0G2h6rOZwqLDTrKk7ksNv7YpAilTnYe1/Q+eDjxEceRMKVWbCsX7t8h6C1qCFi/1Y8WZjcEPBAFG27GPw==",
"dev": true
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"globby": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
"integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6"
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.1.1",
"ignore": "^5.1.4",
"merge2": "^1.3.0",
"slash": "^3.0.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"prebuild-install": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.0.1.tgz",
"integrity": "sha512-7GOJrLuow8yeiyv75rmvZyeMGzl8mdEX5gY69d6a6bHWmiPevwqFw+tQavhK0EYMaSg3/KD24cWqeQv1EWsqDQ==",
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz",
"integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==",
"dev": true,
"requires": {
"detect-libc": "^1.0.3",
@ -13268,21 +13269,39 @@
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^1.0.1",
"node-abi": "^2.7.0",
"noop-logger": "^0.1.1",
"node-abi": "^2.21.0",
"npmlog": "^4.0.1",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^3.0.3",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0",
"which-pm-runs": "^1.0.0"
"tunnel-agent": "^0.6.0"
}
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"resolve": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz",
"integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==",
"dev": true,
"requires": {
"is-core-module": "^2.8.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
@ -13312,55 +13331,90 @@
}
},
"pkg-fetch": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-2.6.9.tgz",
"integrity": "sha512-EnVR8LRILXBvaNP+wJOSY02c3+qDDfyEyR+aqAHLhcc9PBnbxFT9UZ1+If49goPQzQPn26TzF//fc6KXZ0aXEg==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.2.5.tgz",
"integrity": "sha512-jPtyX2VTbG+9yaeGsJEBT+3qVN8qfxxfn7n2lmcx1FDSPsr8jwXDRf6BeCNBV+M5aEQmmtpMeKs7ZILQ9YlSPA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.9.2",
"byline": "^5.0.0",
"chalk": "^3.0.0",
"expand-template": "^2.0.3",
"fs-extra": "^8.1.0",
"minimist": "^1.2.5",
"chalk": "^4.1.2",
"fs-extra": "^9.1.0",
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.6",
"progress": "^2.0.3",
"request": "^2.88.0",
"request-progress": "^3.0.0",
"semver": "^6.3.0",
"unique-temp-dir": "^1.0.0"
"semver": "^7.3.5",
"tar-fs": "^2.1.1",
"yargs": "^16.2.0"
},
"dependencies": {
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"jsonfile": {
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"node-fetch": {
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6"
"whatwg-url": "^5.0.0"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"tar-fs": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
"dev": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
}
}
}
},
@ -13448,11 +13502,11 @@
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
},
"prettyjson": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prettyjson/-/prettyjson-1.2.1.tgz",
"integrity": "sha1-/P+rQdGcq0365eV15kJGYZsS0ok=",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/prettyjson/-/prettyjson-1.2.5.tgz",
"integrity": "sha512-rksPWtoZb2ZpT5OVgtmy0KHVM+Dca3iVwWY9ifwhcexfjebtgjg3wmrUt9PvJ59XIYBcknQeYHD8IAnVlh9lAw==",
"requires": {
"colors": "^1.1.2",
"colors": "1.4.0",
"minimist": "^1.2.0"
}
},
@ -14362,12 +14416,6 @@
}
}
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
},
"regex-not": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
@ -14485,15 +14533,6 @@
}
}
},
"request-progress": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
"integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=",
"dev": true,
"requires": {
"throttleit": "^1.0.0"
}
},
"request-promise": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz",
@ -14677,6 +14716,11 @@
"@types/node": "*"
}
},
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"bl": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz",
@ -14872,11 +14916,35 @@
}
},
"resin-doodles": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/resin-doodles/-/resin-doodles-0.1.1.tgz",
"integrity": "sha1-0AmnndrHEhtFDHkUw2Jip4akBe4=",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/resin-doodles/-/resin-doodles-0.2.0.tgz",
"integrity": "sha512-k6qfrsXoQC9Xj/iRfceMYeFJKvY94H0AzTmrHvneUgAuWQD6nBUDSRLPEqWFOQlEW/mGUYMFSsRYtYDAsUR3ow==",
"requires": {
"colors": "^1.1.2"
"chalk": "^4.1.2"
},
"dependencies": {
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"resin-multibuild": {
@ -16398,6 +16466,12 @@
}
}
},
"supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"svgo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
@ -16746,12 +16820,6 @@
"thenify": ">= 3.1.0 < 4"
}
},
"throttleit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
"integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
"dev": true
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@ -16799,9 +16867,9 @@
}
},
"tmp-promise": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz",
"integrity": "sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
"requires": {
"tmp": "^0.2.0"
}
@ -16873,6 +16941,12 @@
"punycode": "^2.1.1"
}
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
"dev": true
},
"traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
@ -17155,12 +17229,6 @@
"integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==",
"dev": true
},
"uid2": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
"integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=",
"dev": true
},
"unbox-primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
@ -17204,28 +17272,6 @@
"crypto-random-string": "^2.0.0"
}
},
"unique-temp-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz",
"integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=",
"dev": true,
"requires": {
"mkdirp": "^0.5.1",
"os-tmpdir": "^1.0.1",
"uid2": "0.0.3"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
}
}
},
"unit-compare": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unit-compare/-/unit-compare-1.0.1.tgz",
@ -17589,6 +17635,22 @@
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
"integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
"dev": true
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dev": true,
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "13.1.6",
"version": "13.4.1",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -90,7 +90,7 @@
"author": "Balena Inc. (https://balena.io/)",
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0 <13.0.0",
"node": ">=12.8.0 <13.0.0",
"npm": "<7.0.0"
},
"husky": {
@ -127,7 +127,7 @@
"@types/chai-as-promised": "^7.1.4",
"@types/cli-truncate": "^2.0.0",
"@types/common-tags": "^1.8.1",
"@types/dockerode": "^3.3.0",
"@types/dockerode": "^3.3.8",
"@types/ejs": "^3.1.0",
"@types/express": "^4.17.13",
"@types/fs-extra": "^9.0.13",
@ -147,7 +147,7 @@
"@types/ndjson": "^2.0.1",
"@types/net-keepalive": "^0.4.1",
"@types/nock": "^11.1.0",
"@types/node": "^10.17.60",
"@types/node": "^12.20.42",
"@types/node-cleanup": "^2.1.2",
"@types/parse-link-header": "^1.0.1",
"@types/prettyjson": "^0.0.30",
@ -155,6 +155,7 @@
"@types/request": "^2.48.7",
"@types/rewire": "^2.5.28",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9",
"@types/shell-escape": "^0.2.0",
"@types/sinon": "^10.0.6",
"@types/split": "^1.0.0",
@ -184,7 +185,7 @@
"mock-require": "^3.0.3",
"nock": "^13.2.1",
"parse-link-header": "^1.0.1",
"pkg": "^4.5.1",
"pkg": "^5.5.1",
"publish-release": "^1.6.1",
"rewire": "^5.0.0",
"simple-git": "^2.48.0",
@ -206,7 +207,7 @@
"balena-errors": "^4.7.1",
"balena-image-fs": "^7.0.6",
"balena-image-manager": "^7.1.1",
"balena-preload": "^11.0.0",
"balena-preload": "^12.0.0",
"balena-release": "^3.2.0",
"balena-sdk": "^16.9.0",
"balena-semver": "^2.3.0",
@ -223,7 +224,7 @@
"columnify": "^1.5.2",
"common-tags": "^1.7.2",
"denymount": "^2.3.0",
"docker-modem": "^3.0.2",
"docker-modem": "3.0.0",
"docker-progress": "^5.0.1",
"docker-qemu-transpose": "^1.1.1",
"dockerode": "^3.3.1",
@ -257,14 +258,14 @@
"oclif": "^1.18.4",
"open": "^7.1.0",
"patch-package": "^6.4.7",
"prettyjson": "^1.1.3",
"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": "^2.0.2",
"resin-cli-visuals": "^1.8.0",
"resin-compose-parse": "^2.1.3",
"resin-doodles": "^0.1.1",
"resin-doodles": "^0.2.0",
"resin-multibuild": "^4.12.2",
"resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2",
@ -287,6 +288,6 @@
"windosu": "^0.3.0"
},
"versionist": {
"publishedAt": "2022-01-04T16:29:30.222Z"
"publishedAt": "2022-04-11T16:10:47.566Z"
}
}

View File

@ -1,38 +0,0 @@
diff --git a/node_modules/pkg/prelude/bootstrap.js b/node_modules/pkg/prelude/bootstrap.js
index b87902f..58cb7bc 100644
--- a/node_modules/pkg/prelude/bootstrap.js
+++ b/node_modules/pkg/prelude/bootstrap.js
@@ -1220,19 +1220,20 @@ function payloadFileSync(pointer) {
// promises ////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////
- if (fs.promises !== undefined) {
- var util = require('util');
- fs.promises.open = util.promisify(fs.open);
- fs.promises.read = util.promisify(fs.read);
- fs.promises.write = util.promisify(fs.write);
- fs.promises.readFile = util.promisify(fs.readFile);
- fs.promises.readdir = util.promisify(fs.readdir);
- fs.promises.realpath = util.promisify(fs.realpath);
- fs.promises.stat = util.promisify(fs.stat);
- fs.promises.lstat = util.promisify(fs.lstat);
- fs.promises.fstat = util.promisify(fs.fstat);
- fs.promises.access = util.promisify(fs.access);
- }
+ // Disable fs.promises patching as eg `fs.promises.open` returns an object not a number like a promisified `fs.open` returns
+ // if (fs.promises !== undefined) {
+ // var util = require('util');
+ // fs.promises.open = util.promisify(fs.open);
+ // fs.promises.read = util.promisify(fs.read);
+ // fs.promises.write = util.promisify(fs.write);
+ // fs.promises.readFile = util.promisify(fs.readFile);
+ // fs.promises.readdir = util.promisify(fs.readdir);
+ // fs.promises.realpath = util.promisify(fs.realpath);
+ // fs.promises.stat = util.promisify(fs.stat);
+ // fs.promises.lstat = util.promisify(fs.lstat);
+ // fs.promises.fstat = util.promisify(fs.fstat);
+ // fs.promises.access = util.promisify(fs.access);
+ // }
// ///////////////////////////////////////////////////////////////
// INTERNAL //////////////////////////////////////////////////////

View File

@ -32,33 +32,53 @@ describe('balena ssh', function () {
let hasSshExecutable = false;
let mockedExitCode = 0;
async function mockSpawn({ revert = false } = {}) {
const childProcessPath = 'child_process';
if (revert) {
mock.stop(childProcessPath);
mock.reRequire('../../build/utils/ssh');
mock.reRequire('../../build/utils/device/ssh');
return;
}
const { EventEmitter } = await import('stream');
const childProcessMod = await import(childProcessPath);
const originalSpawn = childProcessMod.spawn;
mock(childProcessPath, {
...childProcessMod,
spawn: (program: string, ...args: any[]) => {
if (program.includes('ssh')) {
const emitter = new EventEmitter();
setTimeout(() => emitter.emit('close', mockedExitCode), 1);
return emitter;
}
return originalSpawn(program, ...args);
},
});
}
this.beforeAll(async function () {
hasSshExecutable = await checkSsh();
if (hasSshExecutable) {
[sshServer, sshServerPort] = await startMockSshServer();
if (!hasSshExecutable) {
this.skip();
}
const modPath = '../../build/utils/which';
const mod = await import(modPath);
mock(modPath, {
...mod,
whichSpawn: async () => [mockedExitCode, undefined],
});
[sshServer, sshServerPort] = await startMockSshServer();
await mockSpawn();
});
this.afterAll(function () {
this.afterAll(async function () {
if (sshServer) {
sshServer.close();
sshServer = undefined;
}
mock.stopAll();
await mockSpawn({ revert: true });
});
this.beforeEach(() => {
this.beforeEach(function () {
api = new BalenaAPIMock();
api.expectGetMixpanel({ optional: true });
});
this.afterEach(() => {
this.afterEach(function () {
// Check all expected api calls have been made and clean up.
api.done();
});
@ -87,7 +107,7 @@ describe('balena ssh', function () {
async () => {
const deviceUUID = 'abc1234';
const expectedErrLines = [
'Warning: ssh process exited with non-zero code "255"',
'SSH: Remote command "host abc1234" exited with non-zero status code "255"',
];
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetDevice({ fullUUID: deviceUUID, isOnline: true });
@ -99,22 +119,7 @@ describe('balena ssh', function () {
},
);
it('should produce the expected error message (real ssh, device IP address)', async function () {
if (!hasSshExecutable) {
this.skip();
}
mock.stop('../../build/utils/helpers');
const expectedErrLines = [
'Warning: ssh process exited with non-zero code "255"',
];
const { err, out } = await runCommand(
`ssh 127.0.0.1 -p ${sshServerPort} --noproxy`,
);
expect(cleanOutput(err, true)).to.include.members(expectedErrLines);
expect(out).to.be.empty;
});
it('should fail if device not online (mocked, device UUID)', async () => {
itSS('should fail if device not online (mocked, device UUID)', async () => {
const deviceUUID = 'abc1234';
const expectedErrLines = ['Device with UUID abc1234 is offline'];
api.expectGetWhoAmI({ optional: true, persist: true });
@ -126,6 +131,19 @@ describe('balena ssh', function () {
expect(cleanOutput(err, true)).to.include.members(expectedErrLines);
expect(out).to.be.empty;
});
it('should produce the expected error message (real ssh, device IP address)', async function () {
await mockSpawn({ revert: true });
api.expectGetWhoAmI({ optional: true, persist: true });
const expectedErrLines = [
'SSH: Process exited with non-zero status code "255"',
];
const { err, out } = await runCommand(
`ssh 127.0.0.1 -p ${sshServerPort} --noproxy`,
);
expect(cleanOutput(err, true)).to.include.members(expectedErrLines);
expect(out).to.be.empty;
});
});
/** Check whether the 'ssh' tool (executable) exists in the PATH */
@ -159,9 +177,10 @@ async function startMockSshServer(): Promise<[Server, number]> {
console.error(`mock ssh server error:\n${err}`);
});
return new Promise<[Server, number]>((resolve, reject) => {
// port 0: let the OS allocation any available TCP port number
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
return await new Promise<[Server, number]>((resolve, reject) => {
// TODO: remove 'as any' below. According to @types/node v12.20.42, the
// callback type is `() => void`, but our code assumes `(err: Error) => void`
const listener = (server.listen as any)(0, '127.0.0.1', (err: Error) => {
// this callback is called for the 'listening' event
if (err) {
console.error(`Error starting mock ssh server:\n${err}`);

View File

@ -141,10 +141,10 @@ describe('DeprecationChecker', function () {
getStub.resolves(mockCache);
// Force isTTY to be false (undefined). It happens to be true when
// Force isTTY to be false. It happens to be true when
// the tests run on balenaCI on macOS and Linux.
const originalIsTTY = process.stderr.isTTY;
process.stderr.isTTY = undefined;
process.stderr.isTTY = false;
let result: TestOutput;
try {
result = await runCommand('version');

View File

@ -113,7 +113,9 @@ async function createProxyServer(): Promise<[number, number]> {
let proxyPort = 0; // TCP port number, 0 means automatic allocation
await new Promise<void>((resolve, reject) => {
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
// TODO: remove 'as any' below. According to @types/node v12.20.42, the
// callback type is `() => void`, but our code assumes `(err: Error) => void`
const listener = (server.listen as any)(0, '127.0.0.1', (err: Error) => {
if (err) {
console.error(`Error starting proxy server:\n${err}`);
reject(err);
@ -195,7 +197,9 @@ async function createInterceptorServer(): Promise<number> {
let interceptorPort = 0;
await new Promise<void>((resolve, reject) => {
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
// TODO: remove 'as any' below. According to @types/node v12.20.42, the
// callback type is `() => void`, but our code assumes `(err: Error) => void`
const listener = (server.listen as any)(0, '127.0.0.1', (err: Error) => {
if (err) {
console.error(`Error starting interceptor server:\n${err}`);
reject(err);

137
tests/utils/docker.spec.ts Normal file
View File

@ -0,0 +1,137 @@
/**
* @license
* Copyright 2022 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import {
DockerConnectionCliFlags,
generateConnectOpts,
getDefaultDockerModemOpts,
} from '../../build/utils/docker';
const defaultSocketPath =
process.platform === 'win32'
? '//./pipe/docker_engine'
: '/var/run/docker.sock';
describe('getDefaultDockerModemOpts() function', function () {
it('should use a Unix socket when --dockerHost is not used', () => {
const cliFlags: DockerConnectionCliFlags = {
dockerPort: 2376,
};
const defaultOps = getDefaultDockerModemOpts(cliFlags);
expect(defaultOps).to.deep.include({
host: undefined,
port: undefined,
protocol: 'http',
socketPath: defaultSocketPath,
});
});
it('should use the HTTP protocol when --dockerPort is 2375', () => {
const cliFlags: DockerConnectionCliFlags = {
dockerHost: 'foo',
dockerPort: 2375,
};
const defaultOps = getDefaultDockerModemOpts(cliFlags);
expect(defaultOps).to.deep.include({
host: 'foo',
port: '2375',
protocol: 'http',
socketPath: undefined,
});
});
it('should use the HTTPS protocol when --dockerPort is 2376', () => {
const cliFlags: DockerConnectionCliFlags = {
dockerHost: 'foo',
dockerPort: 2376,
};
const defaultOps = getDefaultDockerModemOpts(cliFlags);
expect(defaultOps).to.deep.include({
host: 'foo',
port: '2376',
protocol: 'https',
socketPath: undefined,
});
});
});
describe('generateConnectOpts() function', function () {
it('should use a Unix socket when --docker is used', async () => {
const cliFlags: DockerConnectionCliFlags = {
docker: 'foo',
};
const connectOpts = await generateConnectOpts(cliFlags);
expect(connectOpts).to.deep.include({
protocol: 'http',
socketPath: 'foo',
});
expect(connectOpts).to.not.have.any.keys('host', 'port');
});
it('should use the HTTP protocol when --dockerPort is 2375', async () => {
const cliFlags: DockerConnectionCliFlags = {
dockerHost: 'foo',
dockerPort: 2375,
};
const connectOpts = await generateConnectOpts(cliFlags);
expect(connectOpts).to.deep.include({
host: 'foo',
port: 2375,
protocol: 'http',
});
expect(connectOpts).to.not.have.any.keys('socketPath');
});
it('should use the HTTPS protocol when --dockerPort is 2376', async () => {
const cliFlags: DockerConnectionCliFlags = {
dockerHost: 'foo',
dockerPort: 2376,
};
const connectOpts = await generateConnectOpts(cliFlags);
expect(connectOpts).to.deep.include({
host: 'foo',
port: 2376,
protocol: 'https',
});
expect(connectOpts).to.not.have.any.keys('socketPath');
});
it('should use the HTTPS protocol when ca/cert/key are used', async () => {
const path = await import('path');
const aFile = path.join(
__dirname,
'../test-data/projects/no-docker-compose/dockerignore1/a.txt',
);
const cliFlags: DockerConnectionCliFlags = {
ca: aFile,
cert: aFile,
key: aFile,
};
const connectOpts = await generateConnectOpts(cliFlags);
expect(connectOpts).to.deep.include({
ca: 'a',
cert: 'a',
key: 'a',
host: undefined,
port: undefined,
protocol: 'https',
socketPath: defaultSocketPath,
});
});
});

View File

@ -0,0 +1,79 @@
/**
* @license
* Copyright 2022 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import * as stripIndent from 'common-tags/lib/stripIndent';
import { getNotifierMessage } from '../../build/utils/update';
import type { UpdateInfo } from 'update-notifier';
describe('getNotifierMessage() unit test', function () {
const template: UpdateInfo = {
current: '',
latest: '',
type: 'latest',
name: '',
};
it('should return a simple update message including installation instructions', () => {
const mockUpdateInfo = {
...template,
current: '12.1.1',
latest: '12.3.0',
};
const msg = getNotifierMessage(mockUpdateInfo);
expect(msg).to.equal(stripIndent`
Update available 12.1.1 → 12.3.0
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md`);
});
it('should include a release notes link when a new major version is available', () => {
const mockUpdateInfo = {
...template,
current: '12.1.1',
latest: '13.3.0',
};
const msg = getNotifierMessage(mockUpdateInfo);
expect(msg).to.equal(stripIndent`
Update available 12.1.1 → 13.3.0
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
Check the v13 release notes at:
https://github.com/balena-io/balena-cli/wiki/CLI-v13-Release-Notes`);
});
it('should return an empty string if no updates are available', () => {
const mockUpdateInfo = {
...template,
current: '12.1.1',
latest: '12.1.1',
};
const msg = getNotifierMessage(mockUpdateInfo);
expect(msg).to.equal('');
});
it('should return an empty string if no updates are available', () => {
const mockUpdateInfo = {
...template,
current: '14.1.1',
latest: '12.1.1',
};
const msg = getNotifierMessage(mockUpdateInfo);
expect(msg).to.equal('');
});
});