mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
1 Commits
v13.7.0
...
output-fra
Author | SHA1 | Date | |
---|---|---|---|
ab1d8aa6ba |
17
.resinci.yml
17
.resinci.yml
@ -7,14 +7,9 @@ npm:
|
||||
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"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "12"
|
||||
- "14"
|
||||
|
File diff suppressed because it is too large
Load Diff
326
CHANGELOG.md
326
CHANGELOG.md
@ -4,332 +4,6 @@ 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.7.0 - 2022-07-07
|
||||
|
||||
* Add `--view` flag to `fleet` command for opening a fleet's dashboard page [Matthew Yarmolinsky]
|
||||
|
||||
## 13.6.1 - 2022-06-09
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to use the native OS release phase & variant fields [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-sdk-16.22.0 - 2022-06-06
|
||||
>
|
||||
> * os: Start using the release.phase field in the available versions [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.21.1 - 2022-06-02
|
||||
>
|
||||
> * Add provisioning key expiry date to generateDeviceProvisioningKey [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-16.21.0 - 2022-06-01
|
||||
>
|
||||
> * os: Refactor the computation of OS releases [Thodoris Greasidis]
|
||||
> * os: Use the model's release variant when the native fields are used [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.20.6 - 2022-06-01
|
||||
>
|
||||
> * Deprecate the needsPasswordReset field of the JWTUser [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.20.5 - 2022-05-25
|
||||
>
|
||||
> * Update TypeScript to v4.7 [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 13.6.0 - 2022-06-06
|
||||
|
||||
* Update QEMU to v7.0.0 [Kyle Harding]
|
||||
|
||||
## 13.5.3 - 2022-05-31
|
||||
|
||||
* Drop the needsPasswordReset property from the tests [Thodoris Greasidis]
|
||||
|
||||
## 13.5.2 - 2022-05-31
|
||||
|
||||
* Deduplicate npm-shrinkwrap.json [Thodoris Greasidis]
|
||||
|
||||
## 13.5.1 - 2022-05-26
|
||||
|
||||
* preload: Fix issue where balenaOS v2.98.3+ required an Internet connection to start apps [pipex]
|
||||
|
||||
## 13.5.0 - 2022-05-24
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to 16.20.4 [Nitish Agarwal] </summary>
|
||||
|
||||
> ### balena-sdk-16.20.4 - 2022-05-09
|
||||
>
|
||||
> * bump @types/node from 10.17.60 to 12.20.500 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.20.3 - 2022-05-06
|
||||
>
|
||||
> * patch: bump browserify from 14.5.0 to 17.0.0 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.20.2 - 2022-05-05
|
||||
>
|
||||
> * patch: bump tmp from 0.0.31 to 0.2.1 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.20.1 - 2022-05-05
|
||||
>
|
||||
> * Drop the non-populated apiUrl & actionsUrl properties from Config type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.20.0 - 2022-05-04
|
||||
>
|
||||
> * models.apiKey: Update apiKeyInfo with expiryDate option [Nitish Agarwal]
|
||||
> * os.getConfig: Add typings for the provisioningKeyExpiryDate option [Balena CI]
|
||||
>
|
||||
> ### balena-sdk-16.19.14 - 2022-05-04
|
||||
>
|
||||
> * config.getAll: Mark the deviceTypes property as optional [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.13 - 2022-05-03
|
||||
>
|
||||
> * patch: bump mocha from 3.5.3 to 10.0.0 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.19.12 - 2022-05-03
|
||||
>
|
||||
> * config.getAll: Deprecate the pubnub property and mark as optional [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.11 - 2022-05-03
|
||||
>
|
||||
> * patch: bump mockttp from 0.9.1 to 2.7.0 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.10 - 2022-04-27
|
||||
>
|
||||
> * Reduce the prod typing dependencies [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.9 - 2022-04-26
|
||||
>
|
||||
> * patch: Remove documentation.md from the NPM package [Vipul Gupta]
|
||||
>
|
||||
> ### balena-sdk-16.19.8 - 2022-04-20
|
||||
>
|
||||
> * patch: Remove additional quotes [Vipul Gupta (@vipulgupta2048)]
|
||||
>
|
||||
> ### balena-sdk-16.19.7 - 2022-04-12
|
||||
>
|
||||
> * tests: Update to work with latest major of superagent [Thodoris Greasidis]
|
||||
> * patch: bump superagent from 3.8.3 to 7.1.2 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.19.6 - 2022-04-11
|
||||
>
|
||||
> * patch: bump dotenv from 4.0.0 to 16.0.0 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.19.5 - 2022-04-09
|
||||
>
|
||||
> * Bump karma to v6 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.4 - 2022-04-09
|
||||
>
|
||||
> * Add dependabot configuration [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.3 - 2022-04-06
|
||||
>
|
||||
> * tests: Update v5 model endpoint prefix references [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.2 - 2022-04-06
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Fix extracting a meaningful error message instead of "[object Object]" [Thodoris Greasidis] </summary>
|
||||
>
|
||||
>> #### balena-request-11.5.5 - 2022-04-06
|
||||
>>
|
||||
>> * Fix extracting the response error from object response bodies [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-request-11.5.4 - 2022-04-06
|
||||
>>
|
||||
>> * Drop explicit karma-chrome-launcher devDependency [Thodoris Greasidis]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-sdk-16.19.1 - 2022-04-05
|
||||
>
|
||||
> * Update balena-request dependency to v11.5.3 [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.19.0 - 2022-03-16
|
||||
>
|
||||
> * Add release.setKnownIssueList function for setting a release's known issue list [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.18.0 - 2022-03-14
|
||||
>
|
||||
> * minor: Add trying SDK in the browser [Vipul Gupta (@vipulgupta2048)]
|
||||
>
|
||||
> ### balena-sdk-16.17.0 - 2022-03-11
|
||||
>
|
||||
> * device.getWithServiceDetails: Add the release id in the service info [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.16.1 - 2022-03-08
|
||||
>
|
||||
> * Replace internal use of deprecated OsVersion.rawVersion with raw_version [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.16.0 - 2022-03-03
|
||||
>
|
||||
> * Add support for named imports from .mjs files [Thodoris Greasidis]
|
||||
> * Update npx command to fix ts-compatibility tests [Thodoris Greasidis]
|
||||
> * Regenerate Documentation [Thodoris Greasidis]
|
||||
> * Update typescript to 4.6.2 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.15.1 - 2022-02-24
|
||||
>
|
||||
> * Remove unnecessary vpn address filtering when fetching local addresses [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sdk-16.15.0 - 2022-02-16
|
||||
>
|
||||
> * Add applicationClass parameter to application create function for setting is_of__class property [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.14.0 - 2022-02-15
|
||||
>
|
||||
> * Add name and description field to generateDeviceKey for device. [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-16.13.4 - 2022-01-27
|
||||
>
|
||||
> * typings: Fix conditional $or/$and/$not $filters [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.13.3 - 2022-01-27
|
||||
>
|
||||
> * Deprecate the supportsBlink field of the DeviceTypeJson.DeviceType type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.13.2 - 2022-01-25
|
||||
>
|
||||
> * Deprecate the logoUrl field of the DeviceTypeJson.DeviceType type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.13.1 - 2022-01-21
|
||||
>
|
||||
> * Replace internal use of release.contains__image with release_image [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.13.0 - 2022-01-21
|
||||
>
|
||||
> * models: Deprecate the release.contains__image in favor of the term form [Thodoris Greasidis]
|
||||
> * models: Add the release_image term form property in the Release typings [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.12.1 - 2022-01-17
|
||||
>
|
||||
> * config.getConfigVarSchema: Send the token only when using a device type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.12.0 - 2022-01-10
|
||||
>
|
||||
> * Replace DeviceTypeJson usage for alias resolution with model queries [Thodoris Greasidis]
|
||||
> * models/device-type: Support aliases as argument of the get() method [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.11.3 - 2022-01-09
|
||||
>
|
||||
> * Fix jsdoc example for balena.errors [Ken Bannister]
|
||||
>
|
||||
> ### balena-sdk-16.11.2 - Invalid date
|
||||
>
|
||||
> * tests: Convert auth spec to async await [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.11.1 - Invalid date
|
||||
>
|
||||
> * Fix buggy tests causing flakiness on node 16 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.11.0 - Invalid date
|
||||
>
|
||||
> * Alias device.getManifestBySlug as config.getDeviceTypeManifestBySlug [Thodoris Greasidis]
|
||||
> * Deprecate device.getManifestByApplication [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.10.0 - Invalid date
|
||||
>
|
||||
> * application.get: Add support for retrieving applications by uuid [Thodoris Greasidis]
|
||||
> * package.json: Rename the lint-fix npm script to lint:fix [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.9.4 - 2021-12-29
|
||||
>
|
||||
> * os: Avoid mutating the args in getAvailableOsVersions & getAllOsVersion [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.9.3 - 2021-12-28
|
||||
>
|
||||
> * os: Replace semver normalization with balena-semver [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.9.2 - 2021-12-28
|
||||
>
|
||||
> * Stop relying on the balena-pine module [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.9.1 - 2021-12-28
|
||||
>
|
||||
> * Enable nested changelogs for balena-hup-action-utils [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Add provisioning key expiry date option to config generate options [Balena CI]
|
||||
|
||||
## 13.4.3 - 2022-05-19
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update docker-progress to 5.1.3 [Pagan Gazzard] </summary>
|
||||
|
||||
> ### docker-progress-5.1.3 - 2022-05-11
|
||||
>
|
||||
> * Reject on the stream closing if it has not already ended successfully [Pagan Gazzard]
|
||||
>
|
||||
> ### docker-progress-5.1.2 - 2022-05-10
|
||||
>
|
||||
> * Update dependencies [Pagan Gazzard]
|
||||
>
|
||||
> ### docker-progress-5.1.1 - 2022-05-10
|
||||
>
|
||||
> * Avoid breaking changes to PushPullOptions required properties [Kyle Harding]
|
||||
>
|
||||
> ### docker-progress-5.1.0 - 2022-03-10
|
||||
>
|
||||
> * Add support for building images with progress [Felipe Lalanne]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 13.4.2 - 2022-05-10
|
||||
|
||||
|
||||
<details>
|
||||
<summary> preload: Fix detection of supervisor version for balenaOS v2.93.0 [Kyle Harding] </summary>
|
||||
|
||||
> ### balena-preload-12.0.1 - 2022-05-10
|
||||
>
|
||||
> * Update supervisor image regex to include tagged images [Kyle Harding]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 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]
|
||||
|
@ -125,39 +125,6 @@ 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
|
||||
|
@ -333,6 +333,30 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
## fleet <fleet>
|
||||
|
||||
Display detailed information about a single fleet.
|
||||
@ -353,7 +377,6 @@ Examples:
|
||||
|
||||
$ balena fleet MyFleet
|
||||
$ balena fleet myorg/myfleet
|
||||
$ balena fleet myorg/myfleet --view
|
||||
|
||||
### Arguments
|
||||
|
||||
@ -363,9 +386,13 @@ fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
#### --view
|
||||
#### --fields FIELDS
|
||||
|
||||
open fleet dashboard page
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
## fleet create <name>
|
||||
|
||||
@ -653,9 +680,29 @@ Examples:
|
||||
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
## devices supported
|
||||
|
||||
@ -674,9 +721,29 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
## device <uuid>
|
||||
|
||||
@ -694,6 +761,14 @@ the device uuid
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
## device deactivate <uuid>
|
||||
|
||||
Deactivate a device.
|
||||
@ -816,10 +891,6 @@ path to the config JSON file, see `balena os build-config`
|
||||
|
||||
custom key name assigned to generated provisioning api key
|
||||
|
||||
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
|
||||
|
||||
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
|
||||
|
||||
## device local-mode <uuid>
|
||||
|
||||
Output current local mode status, or enable/disable local mode
|
||||
@ -1161,6 +1232,14 @@ fleet name or slug (preferred)
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
## release <commitOrId>
|
||||
|
||||
|
||||
@ -1182,6 +1261,14 @@ the commit or ID of the release to get information
|
||||
|
||||
Return the release composition
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
## release finalize <commitOrId>
|
||||
|
||||
Finalize a release. Releases can be "draft" or "final", and this command
|
||||
@ -1280,9 +1367,29 @@ show configuration variables only
|
||||
|
||||
device UUID
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
#### -s, --service SERVICE
|
||||
|
||||
@ -1535,6 +1642,30 @@ device UUID
|
||||
|
||||
release id
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
## tag rm <tagKey>
|
||||
|
||||
Remove a tag from a fleet, device or release.
|
||||
@ -1703,6 +1834,30 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
## key <id>
|
||||
|
||||
Display a single SSH key registered in balenaCloud for the logged in user.
|
||||
@ -1719,6 +1874,14 @@ balenaCloud ID for the SSH key
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
## key add <name> [path]
|
||||
|
||||
Add an SSH key to the balenaCloud account of the logged in user.
|
||||
@ -2245,10 +2408,6 @@ paths to local files to place into the 'system-connections' directory
|
||||
|
||||
custom key name assigned to generated provisioning api key
|
||||
|
||||
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
|
||||
|
||||
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
|
||||
|
||||
## os initialize <image>
|
||||
|
||||
Initialize an os image for a device with a previously
|
||||
@ -2386,10 +2545,6 @@ supervisor cloud polling interval in minutes (e.g. for device variables)
|
||||
|
||||
custom key name assigned to generated provisioning api key
|
||||
|
||||
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
|
||||
|
||||
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
|
||||
|
||||
## config inject <file>
|
||||
|
||||
Inject a 'config.json' file to a balenaOS image file or attached SD card or
|
||||
@ -2411,10 +2566,6 @@ the path to the config.json file to inject
|
||||
|
||||
### Options
|
||||
|
||||
#### -t, --type TYPE
|
||||
|
||||
ignored - no longer required
|
||||
|
||||
#### -d, --drive DRIVE
|
||||
|
||||
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
|
||||
@ -2435,10 +2586,6 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### -t, --type TYPE
|
||||
|
||||
ignored - no longer required
|
||||
|
||||
#### -d, --drive DRIVE
|
||||
|
||||
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
|
||||
@ -2466,10 +2613,6 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### -t, --type TYPE
|
||||
|
||||
ignored - no longer required
|
||||
|
||||
#### -d, --drive DRIVE
|
||||
|
||||
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
|
||||
@ -2508,10 +2651,6 @@ the value of the config parameter to write
|
||||
|
||||
### Options
|
||||
|
||||
#### -t, --type TYPE
|
||||
|
||||
ignored - no longer required
|
||||
|
||||
#### -d, --drive DRIVE
|
||||
|
||||
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
|
||||
@ -2860,6 +2999,14 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
# Local
|
||||
|
||||
## local configure <target>
|
||||
|
@ -171,4 +171,5 @@ export default abstract class BalenaCommand extends Command {
|
||||
|
||||
protected outputMessage = output.outputMessage;
|
||||
protected outputData = output.outputData;
|
||||
protected printTitle = output.printTitle;
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ interface FlagsDef {
|
||||
wifiKey?: string;
|
||||
appUpdatePollInterval?: string;
|
||||
'provisioning-key-name'?: string;
|
||||
'provisioning-key-expiry-date'?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -82,11 +81,7 @@ export default class ConfigGenerateCmd extends Command {
|
||||
dev: cf.dev,
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: [
|
||||
'fleet',
|
||||
'provisioning-key-name',
|
||||
'provisioning-key-expiry-date',
|
||||
],
|
||||
exclusive: ['fleet', 'provisioning-key-name'],
|
||||
},
|
||||
deviceApiKey: flags.string({
|
||||
description:
|
||||
@ -125,11 +120,6 @@ export default class ConfigGenerateCmd extends Command {
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
'provisioning-key-expiry-date': flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -206,7 +196,6 @@ export default class ConfigGenerateCmd extends Command {
|
||||
answers.version = options.version;
|
||||
answers.developmentMode = options.dev;
|
||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
||||
|
||||
// Generate config
|
||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -22,8 +22,10 @@ import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
import type { Application, Release } from 'balena-sdk';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
@ -42,7 +44,7 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
undervoltage_detected?: boolean;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -71,13 +73,16 @@ export default class DeviceCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
@ -163,37 +168,52 @@ export default class DeviceCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
'fleet',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
'cpu_usage_percent',
|
||||
'cpu_temp_c',
|
||||
'cpu_id',
|
||||
'memory_usage_mb',
|
||||
'memory_total_mb',
|
||||
'memory_usage_percent',
|
||||
'storage_block_device',
|
||||
'storage_usage_mb',
|
||||
'storage_total_mb',
|
||||
'storage_usage_percent',
|
||||
'undervoltage_detected',
|
||||
]),
|
||||
);
|
||||
const outputFields = [
|
||||
'device_name',
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
'fleet',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
'cpu_usage_percent',
|
||||
'cpu_temp_c',
|
||||
'cpu_id',
|
||||
'memory_usage_mb',
|
||||
'memory_total_mb',
|
||||
'memory_usage_percent',
|
||||
'storage_block_device',
|
||||
'storage_usage_mb',
|
||||
'storage_total_mb',
|
||||
'storage_usage_percent',
|
||||
'undervoltage_detected',
|
||||
];
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(device, outputFields, {
|
||||
...options,
|
||||
hideNullOrUndefinedValues: true,
|
||||
titleField: 'device_name',
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
outputFields.unshift(`$${device.device_name}$`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(
|
||||
device,
|
||||
outputFields.filter((f) => f !== 'device_name'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ interface FlagsDef {
|
||||
config?: string;
|
||||
help: void;
|
||||
'provisioning-key-name'?: string;
|
||||
'provisioning-key-expiry-date'?: string;
|
||||
}
|
||||
|
||||
export default class DeviceInitCmd extends Command {
|
||||
@ -98,10 +97,6 @@ export default class DeviceInitCmd extends Command {
|
||||
'provisioning-key-name': flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
}),
|
||||
'provisioning-key-expiry-date': flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -190,14 +185,6 @@ export default class DeviceInitCmd extends Command {
|
||||
options['provisioning-key-name'],
|
||||
);
|
||||
}
|
||||
|
||||
if (options['provisioning-key-expiry-date']) {
|
||||
configureCommand.push(
|
||||
'--provisioning-key-expiry-date',
|
||||
options['provisioning-key-expiry-date'],
|
||||
);
|
||||
}
|
||||
|
||||
await runCommand(configureCommand);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,8 +21,10 @@ import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||
|
||||
import type { Application } from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
@ -30,10 +32,10 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
device_type?: string | null;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export default class DevicesCmd extends Command {
|
||||
@ -58,12 +60,11 @@ export default class DevicesCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
fleet: cf.fleet,
|
||||
json: cf.json,
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
@ -99,31 +100,52 @@ export default class DevicesCmd extends Command {
|
||||
return device;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
if (isV14()) {
|
||||
const outputFields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
await this.outputData(devices, outputFields, {
|
||||
...options,
|
||||
displayNullValuesAs: 'N/a',
|
||||
});
|
||||
} else {
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
// Old output implementation
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -17,12 +17,14 @@
|
||||
import { flags } from '@oclif/command';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import type { DataSetOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
json?: boolean;
|
||||
}
|
||||
@ -51,10 +53,7 @@ export default class DevicesSupportedCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
@ -70,7 +69,7 @@ export default class DevicesSupportedCmd extends Command {
|
||||
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
|
||||
interface DT {
|
||||
slug: string;
|
||||
aliases: string[];
|
||||
aliases: string[] | string;
|
||||
arch: string;
|
||||
name: string;
|
||||
}
|
||||
@ -84,19 +83,25 @@ export default class DevicesSupportedCmd extends Command {
|
||||
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
|
||||
deviceTypes.push({
|
||||
slug,
|
||||
aliases: options.json ? aliases : [aliases.join(', ')],
|
||||
aliases: options.json ? aliases : aliases.join(', '),
|
||||
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
|
||||
name: dt.name || 'N/A',
|
||||
});
|
||||
}
|
||||
const fields = ['slug', 'aliases', 'arch', 'name'];
|
||||
deviceTypes = _.sortBy(deviceTypes, fields);
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(deviceTypes, fields, options);
|
||||
} else {
|
||||
const visuals = getVisuals();
|
||||
const output = await visuals.table.horizontal(deviceTypes, fields);
|
||||
console.log(output);
|
||||
// Old output implementation
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
} else {
|
||||
const visuals = getVisuals();
|
||||
const output = await visuals.table.horizontal(deviceTypes, fields);
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -22,12 +22,15 @@ import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
config: boolean;
|
||||
device?: string; // device UUID
|
||||
json: boolean;
|
||||
json?: boolean;
|
||||
help: void;
|
||||
service?: string; // service name
|
||||
}
|
||||
@ -113,7 +116,7 @@ export default class EnvsCmd extends Command {
|
||||
}),
|
||||
device: { ...cf.device, exclusive: ['fleet'] },
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
service: { ...cf.service, exclusive: ['config'] },
|
||||
};
|
||||
|
||||
@ -181,24 +184,59 @@ export default class EnvsCmd extends Command {
|
||||
return i;
|
||||
});
|
||||
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
if (!options.config) {
|
||||
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
|
||||
}
|
||||
if (isV14()) {
|
||||
const results = [...varArray] as any;
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../utils/helpers');
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
// Rename fields
|
||||
if (options.device) {
|
||||
if (options.json) {
|
||||
fields.push('deviceUUID');
|
||||
} else {
|
||||
results.forEach((r: any) => {
|
||||
r.device = r.deviceUUID;
|
||||
delete r.deviceUUID;
|
||||
});
|
||||
|
||||
fields.push('device');
|
||||
}
|
||||
}
|
||||
if (!options.config) {
|
||||
if (options.json) {
|
||||
fields.push('serviceName');
|
||||
} else {
|
||||
results.forEach((r: any) => {
|
||||
r.service = r.serviceName;
|
||||
delete r.serviceName;
|
||||
});
|
||||
fields.push('service');
|
||||
}
|
||||
}
|
||||
|
||||
await this.outputData(results, fields, {
|
||||
...options,
|
||||
sort: options.sort || 'name',
|
||||
});
|
||||
} else {
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
// Old output implementation
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
if (!options.config) {
|
||||
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../utils/helpers');
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { flags as flagsType } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { Release } from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
@ -29,7 +28,6 @@ import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
view: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -47,19 +45,14 @@ export default class FleetCmd extends Command {
|
||||
public static examples = [
|
||||
'$ balena fleet MyFleet',
|
||||
'$ balena fleet myorg/myfleet',
|
||||
'$ balena fleet myorg/myfleet --view',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet <fleet>';
|
||||
|
||||
public static flags: flagsType.Input<FlagsDef> = {
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
view: flags.boolean({
|
||||
default: false,
|
||||
description: 'open fleet dashboard page',
|
||||
}),
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
@ -73,9 +66,7 @@ export default class FleetCmd extends Command {
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const application = (await getApplication(balena, params.fleet, {
|
||||
const application = (await getApplication(getBalenaSdk(), params.fleet, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
@ -87,15 +78,6 @@ export default class FleetCmd extends Command {
|
||||
commit?: string;
|
||||
};
|
||||
|
||||
if (options.view) {
|
||||
const open = await import('open');
|
||||
const dashboardUrl = balena.models.application.getDashboardUrl(
|
||||
application.id,
|
||||
);
|
||||
await open(dashboardUrl, { wait: false });
|
||||
return;
|
||||
}
|
||||
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
application.commit = application.should_be_running__release[0]?.commit;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -20,10 +20,13 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -52,27 +55,52 @@ export default class KeyCmd extends Command {
|
||||
public static usage = 'key <id>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
KeyCmd,
|
||||
);
|
||||
|
||||
const key = await getBalenaSdk().models.key.get(params.id);
|
||||
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
};
|
||||
if (isV14()) {
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
public_key: key.public_key,
|
||||
};
|
||||
|
||||
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
|
||||
if (!options.json) {
|
||||
// Id is redundant, since user must have provided it in command call
|
||||
this.printTitle(displayKey.name);
|
||||
this.outputMessage(displayKey.public_key);
|
||||
} else {
|
||||
await this.outputData(
|
||||
displayKey,
|
||||
['id', 'name', 'public_key'],
|
||||
options,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Old output implementation
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
};
|
||||
|
||||
// Since the public key string is long, it might
|
||||
// wrap to lines below, causing the table layout to break.
|
||||
// See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key);
|
||||
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
|
||||
|
||||
// Since the public key string is long, it might
|
||||
// wrap to lines below, causing the table layout to break.
|
||||
// See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-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.
|
||||
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -35,13 +38,14 @@ export default class KeysCmd extends Command {
|
||||
public static usage = 'keys';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(KeysCmd);
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(KeysCmd);
|
||||
|
||||
const keys = await getBalenaSdk().models.key.getAll();
|
||||
|
||||
@ -50,6 +54,12 @@ export default class KeysCmd extends Command {
|
||||
return { id: k.id, name: k.title };
|
||||
});
|
||||
|
||||
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
|
||||
// Display
|
||||
if (isV14()) {
|
||||
await this.outputData(displayKeys, ['id', 'name'], options);
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-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.
|
||||
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -36,12 +39,13 @@ export default class OrgsCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(OrgsCmd);
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(OrgsCmd);
|
||||
|
||||
const { getOwnOrganizations } = await import('../utils/sdk');
|
||||
|
||||
@ -49,8 +53,13 @@ export default class OrgsCmd extends Command {
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk());
|
||||
|
||||
// Display
|
||||
console.log(
|
||||
getVisuals().table.horizontal(organizations, ['name', 'handle']),
|
||||
);
|
||||
if (isV14()) {
|
||||
await this.outputData(organizations, ['name', 'handle'], options);
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(
|
||||
getVisuals().table.horizontal(organizations, ['name', 'handle']),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ interface FlagsDef {
|
||||
'system-connection': string[];
|
||||
'initial-device-name'?: string;
|
||||
'provisioning-key-name'?: string;
|
||||
'provisioning-key-expiry-date'?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -59,7 +58,6 @@ interface Answers {
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
provisioningKeyName?: string;
|
||||
provisioningKeyExpiryDate?: string;
|
||||
}
|
||||
|
||||
export default class OsConfigureCmd extends Command {
|
||||
@ -123,7 +121,7 @@ export default class OsConfigureCmd extends Command {
|
||||
config: flags.string({
|
||||
description:
|
||||
'path to a pre-generated config.json file to be injected in the OS image',
|
||||
exclusive: ['provisioning-key-name', 'provisioning-key-expiry-date'],
|
||||
exclusive: ['provisioning-key-name'],
|
||||
}),
|
||||
'config-app-update-poll-interval': flags.integer({
|
||||
description:
|
||||
@ -140,14 +138,7 @@ export default class OsConfigureCmd extends Command {
|
||||
description: 'WiFi SSID (network name) (non-interactive configuration)',
|
||||
}),
|
||||
dev: cf.dev,
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: [
|
||||
'fleet',
|
||||
'provisioning-key-name',
|
||||
'provisioning-key-expiry-date',
|
||||
],
|
||||
},
|
||||
device: { ...cf.device, exclusive: ['fleet', 'provisioning-key-name'] },
|
||||
'device-type': flags.string({
|
||||
description:
|
||||
'device type slug (e.g. "raspberrypi3") to override the fleet device type',
|
||||
@ -170,11 +161,6 @@ export default class OsConfigureCmd extends Command {
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
exclusive: ['config', 'device'],
|
||||
}),
|
||||
'provisioning-key-expiry-date': flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
exclusive: ['config', 'device'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -249,7 +235,6 @@ export default class OsConfigureCmd extends Command {
|
||||
answers.version = osVersion;
|
||||
answers.developmentMode = options.dev;
|
||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
||||
|
||||
if (_.isEmpty(configJson)) {
|
||||
if (device) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -22,8 +22,11 @@ import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import jsyaml = require('js-yaml');
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
composition?: boolean;
|
||||
}
|
||||
@ -49,7 +52,9 @@ export default class ReleaseCmd extends Command {
|
||||
default: false,
|
||||
char: 'c',
|
||||
description: 'Return the release composition',
|
||||
exclusive: ['json', 'fields'],
|
||||
}),
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static args = [
|
||||
@ -68,29 +73,27 @@ export default class ReleaseCmd extends Command {
|
||||
ReleaseCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
if (options.composition) {
|
||||
await this.showComposition(params.commitOrId, balena);
|
||||
await this.showComposition(params.commitOrId);
|
||||
} else {
|
||||
await this.showReleaseInfo(params.commitOrId, balena);
|
||||
await this.showReleaseInfo(params.commitOrId, options);
|
||||
}
|
||||
}
|
||||
|
||||
async showComposition(
|
||||
commitOrId: string | number,
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
) {
|
||||
const release = await balena.models.release.get(commitOrId, {
|
||||
async showComposition(commitOrId: string | number) {
|
||||
const release = await getBalenaSdk().models.release.get(commitOrId, {
|
||||
$select: 'composition',
|
||||
});
|
||||
|
||||
console.log(jsyaml.dump(release.composition));
|
||||
if (isV14()) {
|
||||
this.outputMessage(jsyaml.dump(release.composition));
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(jsyaml.dump(release.composition));
|
||||
}
|
||||
}
|
||||
|
||||
async showReleaseInfo(
|
||||
commitOrId: string | number,
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
) {
|
||||
async showReleaseInfo(commitOrId: string | number, options: FlagsDef) {
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
'commit',
|
||||
@ -103,7 +106,7 @@ export default class ReleaseCmd extends Command {
|
||||
'end_timestamp',
|
||||
];
|
||||
|
||||
const release = await balena.models.release.get(commitOrId, {
|
||||
const release = await getBalenaSdk().models.release.get(commitOrId, {
|
||||
$select: fields,
|
||||
$expand: {
|
||||
release_tag: {
|
||||
@ -116,13 +119,28 @@ export default class ReleaseCmd extends Command {
|
||||
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
|
||||
.join('\n');
|
||||
|
||||
const _ = await import('lodash');
|
||||
const values = _.mapValues(
|
||||
release,
|
||||
(val) => val ?? 'N/a',
|
||||
) as Dictionary<string>;
|
||||
values['tags'] = tagStr;
|
||||
if (isV14()) {
|
||||
await this.outputData(
|
||||
{
|
||||
tags: tagStr,
|
||||
...release,
|
||||
},
|
||||
fields,
|
||||
{
|
||||
displayNullValuesAs: 'N/a',
|
||||
...options,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Old output implementation
|
||||
const _ = await import('lodash');
|
||||
const values = _.mapValues(
|
||||
release,
|
||||
(val) => val ?? 'N/a',
|
||||
) as Dictionary<string>;
|
||||
values['tags'] = tagStr;
|
||||
|
||||
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
|
||||
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,8 +21,11 @@ import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationNameNote } from '../utils/messages';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -43,6 +46,7 @@ export default class ReleasesCmd extends Command {
|
||||
public static usage = 'releases <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -57,7 +61,9 @@ export default class ReleasesCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ReleasesCmd,
|
||||
);
|
||||
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
@ -76,12 +82,20 @@ export default class ReleasesCmd extends Command {
|
||||
{ $select: fields },
|
||||
);
|
||||
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
if (isV14()) {
|
||||
await this.outputData(releases, fields, {
|
||||
displayNullValuesAs: 'N/a',
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import type { DataOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -35,15 +38,27 @@ export default class SettingsCmd extends Command {
|
||||
public static usage = 'settings';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(SettingsCmd);
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(SettingsCmd);
|
||||
|
||||
const settings = await getBalenaSdk().settings.getAll();
|
||||
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(settings));
|
||||
if (isV14()) {
|
||||
// Select all available fields for display
|
||||
const fields = Object.keys(settings);
|
||||
|
||||
await this.outputData(settings, fields, {
|
||||
noCapitalizeKeys: true,
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(settings));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ 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;
|
||||
@ -127,8 +128,8 @@ export default class SshCmd extends Command {
|
||||
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
|
||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||
return await performLocalDeviceSSH({
|
||||
hostname: params.fleetOrDevice,
|
||||
port: options.port || 'local',
|
||||
address: params.fleetOrDevice,
|
||||
port: options.port,
|
||||
forceTTY: options.tty,
|
||||
verbose: options.verbose,
|
||||
service: params.service,
|
||||
@ -151,6 +152,12 @@ 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([
|
||||
@ -202,15 +209,19 @@ export default class SshCmd extends Command {
|
||||
// that we know exists and is accessible
|
||||
let containerId: string | undefined;
|
||||
if (params.service != null) {
|
||||
const { getContainerIdForService } = await import('../utils/device/ssh');
|
||||
containerId = await getContainerIdForService({
|
||||
containerId = await this.getContainerId(
|
||||
sdk,
|
||||
deviceUuid,
|
||||
hostname: `ssh.${proxyUrl}`,
|
||||
port: options.port || 'cloud',
|
||||
proxyCommand,
|
||||
service: params.service,
|
||||
username: username!,
|
||||
});
|
||||
params.service,
|
||||
{
|
||||
port: options.port,
|
||||
proxyCommand,
|
||||
proxyUrl: proxyUrl || '',
|
||||
username: username!,
|
||||
},
|
||||
supervisorVersion,
|
||||
deviceId,
|
||||
);
|
||||
}
|
||||
|
||||
let accessCommand: string;
|
||||
@ -219,14 +230,158 @@ export default class SshCmd extends Command {
|
||||
} else {
|
||||
accessCommand = `host ${deviceUuid}`;
|
||||
}
|
||||
const { runRemoteCommand } = await import('../utils/ssh');
|
||||
await runRemoteCommand({
|
||||
cmd: accessCommand,
|
||||
hostname: `ssh.${proxyUrl}`,
|
||||
port: options.port || 'cloud',
|
||||
proxyCommand,
|
||||
username,
|
||||
|
||||
const command = this.generateVpnSshCommand({
|
||||
uuid: deviceUuid,
|
||||
command: accessCommand,
|
||||
verbose: options.verbose,
|
||||
port: options.port,
|
||||
proxyCommand,
|
||||
proxyUrl: proxyUrl || '',
|
||||
username: username!,
|
||||
});
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,8 +21,12 @@ import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import type { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
@ -61,6 +65,7 @@ export default class TagsCmd extends Command {
|
||||
...cf.release,
|
||||
exclusive: ['fleet', 'device'],
|
||||
},
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -78,7 +83,7 @@ export default class TagsCmd extends Command {
|
||||
|
||||
const { tryAsInteger } = await import('../utils/validation');
|
||||
|
||||
let tags;
|
||||
let tags: ApplicationTag[] | DeviceTag[] | ReleaseTag[] = [];
|
||||
|
||||
if (options.fleet) {
|
||||
const { getFleetSlug } = await import('../utils/sdk');
|
||||
@ -103,11 +108,17 @@ export default class TagsCmd extends Command {
|
||||
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
|
||||
}
|
||||
|
||||
if (!tags || tags.length === 0) {
|
||||
if (tags.length === 0 && !options.json) {
|
||||
// TODO: Later change to output message
|
||||
throw new ExpectedError('No tags found');
|
||||
}
|
||||
|
||||
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
|
||||
if (isV14()) {
|
||||
await this.outputData(tags, ['tag_key', 'value'], options);
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
|
||||
}
|
||||
}
|
||||
|
||||
protected missingResourceMessage = stripIndent`
|
||||
|
@ -136,7 +136,8 @@ export default class TunnelCmd extends Command {
|
||||
// Ascertain device uuid
|
||||
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
|
||||
const uuid = await getOnlineTargetDeviceUuid(sdk, params.deviceOrFleet);
|
||||
logger.logInfo(`Opening a tunnel to ${uuid}...`);
|
||||
const device = await sdk.models.device.get(uuid);
|
||||
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
|
||||
|
||||
const _ = await import('lodash');
|
||||
const localListeners = _.chain(options.port)
|
||||
@ -146,7 +147,11 @@ export default class TunnelCmd extends Command {
|
||||
.map(async ({ localPort, localAddress, remotePort }) => {
|
||||
try {
|
||||
const { tunnelConnectionToDevice } = await import('../utils/tunnel');
|
||||
const handler = await tunnelConnectionToDevice(uuid, remotePort, sdk);
|
||||
const handler = await tunnelConnectionToDevice(
|
||||
device.uuid,
|
||||
remotePort,
|
||||
sdk,
|
||||
);
|
||||
|
||||
const { createServer } = await import('net');
|
||||
const server = createServer(async (client: Socket) => {
|
||||
@ -157,7 +162,7 @@ export default class TunnelCmd extends Command {
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
uuid,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
);
|
||||
} catch (err) {
|
||||
@ -166,7 +171,7 @@ export default class TunnelCmd extends Command {
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
uuid,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
err,
|
||||
);
|
||||
@ -181,15 +186,15 @@ export default class TunnelCmd extends Command {
|
||||
});
|
||||
|
||||
logger.logInfo(
|
||||
` - tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}`,
|
||||
` - tunnelling ${localAddress}:${localPort} to ${device.uuid}:${remotePort}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.logWarn(
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}, failed ${JSON.stringify(
|
||||
err.message,
|
||||
)}`,
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
|
||||
);
|
||||
|
||||
return false;
|
||||
|
@ -16,7 +16,12 @@
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import { getBalenaSdk, stripIndent } from './utils/lazy';
|
||||
|
||||
interface CachedUsername {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track balena CLI usage events (product improvement analytics).
|
||||
@ -44,13 +49,40 @@ export async function trackCommand(commandSignature: string) {
|
||||
scope.setExtra('command', commandSignature);
|
||||
});
|
||||
}
|
||||
const { getCachedUsername } = await import('./utils/bootstrap');
|
||||
let username: string | undefined;
|
||||
try {
|
||||
username = (await getCachedUsername())?.username;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
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;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry!.configureScope((scope) => {
|
||||
scope.setUser({
|
||||
@ -64,7 +96,6 @@ 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);
|
||||
}
|
||||
|
@ -1,26 +1,35 @@
|
||||
/*
|
||||
Copyright 2020 Balena
|
||||
|
||||
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.
|
||||
*/
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 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 { getCliUx, getChalk } from '../utils/lazy';
|
||||
|
||||
/**
|
||||
* Used to extend FlagsDef for commands that output single-record data.
|
||||
* Exposed to user in command options.
|
||||
*/
|
||||
export interface DataOutputOptions {
|
||||
fields?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to extend FlagsDef for commands that output multi-record data.
|
||||
* Exposed to user in command options.
|
||||
*/
|
||||
export interface DataSetOutputOptions extends DataOutputOptions {
|
||||
filter?: string;
|
||||
'no-header'?: boolean;
|
||||
@ -28,6 +37,14 @@ export interface DataSetOutputOptions extends DataOutputOptions {
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
// Not exposed to user
|
||||
export interface InternalOutputOptions {
|
||||
displayNullValuesAs?: string;
|
||||
hideNullOrUndefinedValues?: boolean;
|
||||
titleField?: string;
|
||||
noCapitalizeKeys?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output message to STDERR
|
||||
*/
|
||||
@ -49,7 +66,7 @@ export function outputMessage(msg: string) {
|
||||
export async function outputData(
|
||||
data: any[] | {},
|
||||
fields: string[],
|
||||
options: DataOutputOptions | DataSetOutputOptions,
|
||||
options: (DataOutputOptions | DataSetOutputOptions) & InternalOutputOptions,
|
||||
) {
|
||||
if (Array.isArray(data)) {
|
||||
await outputDataSet(data, fields, options as DataSetOutputOptions);
|
||||
@ -68,7 +85,7 @@ export async function outputData(
|
||||
async function outputDataSet(
|
||||
data: any[],
|
||||
fields: string[],
|
||||
options: DataSetOutputOptions,
|
||||
options: DataSetOutputOptions & InternalOutputOptions,
|
||||
) {
|
||||
// Oclif expects fields to be specified in the format used in table headers (though lowercase)
|
||||
// By replacing underscores with spaces here, we can support both header format and actual field name
|
||||
@ -77,6 +94,12 @@ async function outputDataSet(
|
||||
options.filter = options.filter?.replace(/_/g, ' ');
|
||||
options.sort = options.sort?.replace(/_/g, ' ');
|
||||
|
||||
if (!options.json) {
|
||||
data = data.map((d) => {
|
||||
return processNullValues(d, options);
|
||||
});
|
||||
}
|
||||
|
||||
getCliUx().table(
|
||||
data,
|
||||
// Convert fields array to column object keys
|
||||
@ -97,7 +120,7 @@ async function outputDataSet(
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a single data object (like `resin-cli-visuals table.vertical`),
|
||||
* Outputs a single data object (similar to `resin-cli-visuals table.vertical`),
|
||||
* but supporting a subset of options from `cli-ux table` (--json and --fields)
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
@ -107,9 +130,9 @@ async function outputDataSet(
|
||||
async function outputDataItem(
|
||||
data: any,
|
||||
fields: string[],
|
||||
options: DataOutputOptions,
|
||||
options: DataOutputOptions & InternalOutputOptions,
|
||||
) {
|
||||
const outData: typeof data = {};
|
||||
let outData: typeof data = {};
|
||||
|
||||
// Convert comma separated list of fields in `options.fields` to array of correct format.
|
||||
// Note, user may have specified the true field name (e.g. `some_field`),
|
||||
@ -125,30 +148,83 @@ async function outputDataItem(
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
(options.displayNullValuesAs || options.hideNullOrUndefinedValues) &&
|
||||
!options.json
|
||||
) {
|
||||
outData = processNullValues(outData, options);
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
printLine(JSON.stringify(outData, undefined, 2));
|
||||
} else {
|
||||
const chalk = getChalk();
|
||||
const { capitalize } = await import('lodash');
|
||||
|
||||
// Find longest key, so we can align results
|
||||
const longestKeyLength = getLongestObjectKeyLength(outData);
|
||||
|
||||
if (options.titleField) {
|
||||
printTitle(data[options.titleField as keyof any[]], options);
|
||||
}
|
||||
|
||||
// Output one field per line
|
||||
for (const [k, v] of Object.entries(outData)) {
|
||||
for (let [k, v] of Object.entries(outData)) {
|
||||
const shim = ' '.repeat(longestKeyLength - k.length);
|
||||
const kDisplay = capitalize(k.replace(/_/g, ' '));
|
||||
printLine(`${chalk.bold(kDisplay) + shim} : ${v}`);
|
||||
let kDisplay = k.replace(/_/g, ' ');
|
||||
|
||||
// Start multiline values on the line below the field name
|
||||
if (typeof v === 'string' && v.includes('\n')) {
|
||||
v = `\n${v}`;
|
||||
}
|
||||
|
||||
if (!options.noCapitalizeKeys) {
|
||||
kDisplay = capitalize(kDisplay);
|
||||
}
|
||||
if (k !== options.titleField) {
|
||||
printLine(` ${bold(kDisplay) + shim} : ${v}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLongestObjectKeyLength(o: any): number {
|
||||
return Object.keys(o).length >= 1
|
||||
? Object.keys(o).reduce((a, b) => {
|
||||
return a.length > b.length ? a : b;
|
||||
}).length
|
||||
: 0;
|
||||
/**
|
||||
* Amend null/undefined values in data as per options:
|
||||
* - options.displayNullValuesAs will replace the value with the specified string
|
||||
* - options.hideNullOrUndefinedValues will remove the property from the data
|
||||
*
|
||||
* @param data The data object to process
|
||||
* @param options Output options
|
||||
*
|
||||
* @returns a copy of the data with amended values.
|
||||
*/
|
||||
function processNullValues(data: any, options: InternalOutputOptions) {
|
||||
const dataCopy = { ...data };
|
||||
|
||||
Object.entries(dataCopy).forEach(([k, v]) => {
|
||||
if (v == null) {
|
||||
if (options.displayNullValuesAs) {
|
||||
dataCopy[k] = options.displayNullValuesAs;
|
||||
} else if (options.hideNullOrUndefinedValues) {
|
||||
delete dataCopy[k];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return dataCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a title with underscore
|
||||
*
|
||||
* @param title The title string to print
|
||||
* @param options Output options
|
||||
*/
|
||||
export function printTitle(
|
||||
title: string,
|
||||
options?: InternalOutputOptions & DataSetOutputOptions,
|
||||
) {
|
||||
if (!options?.['no-header']) {
|
||||
printLine(` ${capitalize(bold(title))}`);
|
||||
printLine(` ${bold('─'.repeat(title.length))}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printLine(s: any) {
|
||||
@ -156,3 +232,15 @@ function printLine(s: any) {
|
||||
// but using this one explicitly for ease of testing
|
||||
process.stdout.write(s + '\n');
|
||||
}
|
||||
|
||||
function capitalize(s: string) {
|
||||
return `${s[0].toUpperCase()}${s.slice(1)}`;
|
||||
}
|
||||
|
||||
function bold(s: string) {
|
||||
return getChalk().bold(s);
|
||||
}
|
||||
|
||||
function getLongestObjectKeyLength(o: any): number {
|
||||
return Math.max(0, ...Object.keys(o).map((k) => k.length));
|
||||
}
|
||||
|
@ -53,6 +53,12 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
||||
if (extractBooleanFlag(cmdSlice, '--debug')) {
|
||||
process.env.DEBUG = '1';
|
||||
}
|
||||
// support global --v-next flag
|
||||
if (extractBooleanFlag(cmdSlice, '--v-next')) {
|
||||
const { version } = await import('../package.json');
|
||||
const { inc } = await import('semver');
|
||||
process.env.BALENA_CLI_VERSION_OVERRIDE = inc(version, 'major') || '';
|
||||
}
|
||||
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
|
||||
}
|
||||
|
||||
|
@ -119,61 +119,3 @@ 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;
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ 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';
|
||||
|
||||
@ -1349,6 +1348,9 @@ 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,
|
||||
@ -1371,10 +1373,10 @@ export async function deployProject(
|
||||
|
||||
const contractPath = path.join(projectPath, 'balena.yml');
|
||||
const contract = await getContractContent(contractPath);
|
||||
if (contract?.version && !semver.valid(contract.version)) {
|
||||
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
|
||||
throw new ExpectedError(stripIndent`\
|
||||
Error: the version field in "${contractPath}"
|
||||
is not a valid semver`);
|
||||
Error: expected the version field in "${contractPath}"
|
||||
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
|
||||
}
|
||||
|
||||
const $release = await runSpinner(
|
||||
|
@ -13,140 +13,89 @@ 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 { ExpectedError } from '../../errors';
|
||||
import type { ContainerInfo } from 'dockerode';
|
||||
import { stripIndent } from '../lazy';
|
||||
|
||||
import {
|
||||
findBestUsernameForDevice,
|
||||
getRemoteCommandOutput,
|
||||
runRemoteCommand,
|
||||
SshRemoteCommandOpts,
|
||||
} from '../ssh';
|
||||
|
||||
export interface DeviceSSHOpts extends SshRemoteCommandOpts {
|
||||
export interface DeviceSSHOpts {
|
||||
address: string;
|
||||
port?: number;
|
||||
forceTTY?: boolean;
|
||||
verbose: boolean;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
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 const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
|
||||
|
||||
export async function performLocalDeviceSSH(
|
||||
opts: DeviceSSHOpts,
|
||||
): Promise<void> {
|
||||
// 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 = '';
|
||||
const { escapeRegExp, reduce } = await import('lodash');
|
||||
const { spawnSshAndThrowOnError } = await import('../ssh');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
if (opts.service) {
|
||||
const containerId = await getContainerIdForService({
|
||||
...opts,
|
||||
service: opts.service,
|
||||
username,
|
||||
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,
|
||||
});
|
||||
|
||||
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
|
||||
@ -154,8 +103,17 @@ 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' : '';
|
||||
cmd = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
|
||||
command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
|
||||
}
|
||||
|
||||
await runRemoteCommand({ ...opts, cmd, username });
|
||||
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] : []),
|
||||
]);
|
||||
}
|
||||
|
@ -174,8 +174,14 @@ 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: DockerConnectionCliFlags,
|
||||
options: ExtendedDockerOptions,
|
||||
): Promise<dockerode> {
|
||||
const connectOpts = await generateConnectOpts(options);
|
||||
const client = await createClient(connectOpts);
|
||||
@ -190,18 +196,14 @@ export async function createClient(
|
||||
return new Docker(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = {};
|
||||
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();
|
||||
const optsOfInterest: Array<keyof dockerode.DockerOptions> = [
|
||||
'ca',
|
||||
'cert',
|
||||
@ -213,33 +215,9 @@ export function getDefaultDockerModemOpts(
|
||||
'username',
|
||||
'timeout',
|
||||
];
|
||||
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;
|
||||
}
|
||||
for (const opt of optsOfInterest) {
|
||||
connectOpts[opt] = defaultOpts[opt];
|
||||
}
|
||||
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) {
|
||||
@ -263,9 +241,9 @@ export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
|
||||
// These should be file paths (strings)
|
||||
const tlsOpts = [opts.ca, opts.cert, opts.key];
|
||||
|
||||
// If any tlsOpts are set...
|
||||
// If any 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',
|
||||
@ -280,11 +258,7 @@ export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
|
||||
const [ca, cert, key] = await Promise.all(
|
||||
tlsOpts.map((opt: string) => fs.readFile(opt, 'utf8')),
|
||||
);
|
||||
// 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' };
|
||||
connectOpts = { ...connectOpts, ca, cert, key };
|
||||
}
|
||||
|
||||
return connectOpts;
|
||||
|
@ -20,7 +20,7 @@ import { ExpectedError, printErrorMessage } from '../errors';
|
||||
import { getVisuals, stripIndent, getCliForm } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
import { confirm } from './patterns';
|
||||
import { getLocalDeviceCmdStdout, getDeviceOsRelease } from './ssh';
|
||||
import { exec, execBuffered, getDeviceOsRelease } from './ssh';
|
||||
|
||||
const MIN_BALENAOS_VERSION = 'v2.14.0';
|
||||
|
||||
@ -80,12 +80,7 @@ export async function leave(
|
||||
logger.logDebug('Deconfiguring...');
|
||||
await deconfigure(deviceHostnameOrIp);
|
||||
|
||||
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.`);
|
||||
logger.logSuccess('Device successfully left the platform.');
|
||||
}
|
||||
|
||||
async function execCommand(
|
||||
@ -93,25 +88,20 @@ async function execCommand(
|
||||
cmd: string,
|
||||
msg: string,
|
||||
): Promise<void> {
|
||||
const { Writable } = await import('stream');
|
||||
const through = await import('through2');
|
||||
const visuals = getVisuals();
|
||||
|
||||
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
|
||||
const innerSpinner = spinner.spinner;
|
||||
|
||||
const stream = new Writable({
|
||||
write(_chunk: Buffer, _enc, callback) {
|
||||
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
|
||||
callback();
|
||||
},
|
||||
const stream = through(function (data, _enc, cb) {
|
||||
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
|
||||
cb(null, data);
|
||||
});
|
||||
|
||||
spinner.start();
|
||||
try {
|
||||
await getLocalDeviceCmdStdout(deviceIp, cmd, stream);
|
||||
} finally {
|
||||
spinner.stop();
|
||||
}
|
||||
await exec(deviceIp, cmd, stream);
|
||||
spinner.stop();
|
||||
}
|
||||
|
||||
async function configure(deviceIp: string, config: any): Promise<void> {
|
||||
@ -131,7 +121,7 @@ async function deconfigure(deviceIp: string): Promise<void> {
|
||||
async function assertDeviceIsCompatible(deviceIp: string): Promise<void> {
|
||||
const cmd = 'os-config --version';
|
||||
try {
|
||||
await getLocalDeviceCmdStdout(deviceIp, cmd);
|
||||
await execBuffered(deviceIp, cmd);
|
||||
} catch (err) {
|
||||
if (err instanceof ExpectedError) {
|
||||
throw err;
|
||||
|
@ -21,7 +21,7 @@ import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, stripIndent } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
|
||||
export const QEMU_VERSION = 'v7.0.0+balena1';
|
||||
export const QEMU_VERSION = 'v6.0.0+balena1';
|
||||
export const QEMU_BIN_NAME = 'qemu-execve';
|
||||
|
||||
export function qemuPathInContext(context: string) {
|
||||
|
377
lib/utils/ssh.ts
377
lib/utils/ssh.ts
@ -16,314 +16,147 @@
|
||||
*/
|
||||
import { spawn, StdioOptions } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import { TypedError } from 'typed-error';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
export class SshPermissionDeniedError extends ExpectedError {}
|
||||
export class ExecError extends TypedError {
|
||||
public cmd: string;
|
||||
public exitCode: number;
|
||||
|
||||
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));
|
||||
constructor(cmd: string, exitCode: number) {
|
||||
super(`Command '${cmd}' failed with error: ${exitCode}`);
|
||||
this.cmd = cmd;
|
||||
this.exitCode = exitCode;
|
||||
this.exitSignal = exitSignal;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
export async function exec(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
stdout?: NodeJS.WritableStream,
|
||||
): Promise<void> {
|
||||
const { which } = await import('./which');
|
||||
const program = await which('ssh');
|
||||
const args = sshArgsForRemoteCommand({
|
||||
const args = [
|
||||
'-n',
|
||||
'-t',
|
||||
'-p',
|
||||
'22222',
|
||||
'-o',
|
||||
'LogLevel=ERROR',
|
||||
'-o',
|
||||
'StrictHostKeyChecking=no',
|
||||
'-o',
|
||||
'UserKnownHostsFile=/dev/null',
|
||||
`root@${deviceIp}`,
|
||||
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 = [
|
||||
typeof stdin === 'string' ? stdin : 'pipe',
|
||||
typeof stdout === 'string' ? stdout : 'pipe',
|
||||
typeof stderr === 'string' ? stderr : 'pipe',
|
||||
'inherit',
|
||||
stdout ? 'pipe' : 'inherit',
|
||||
'inherit',
|
||||
];
|
||||
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]));
|
||||
|
||||
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);
|
||||
const exitCode = await new Promise<number>((resolve, reject) => {
|
||||
const ps = spawn(program, args, { stdio })
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
|
||||
if (stdout && ps.stdout) {
|
||||
ps.stdout.pipe(stdout);
|
||||
}
|
||||
});
|
||||
if (exitCode !== 0) {
|
||||
throw new ExecError(cmd, exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
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,
|
||||
export async function execBuffered(
|
||||
deviceIp: 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;
|
||||
enc?: string,
|
||||
): Promise<string> {
|
||||
const through = await import('through2');
|
||||
const buffer: string[] = [];
|
||||
await exec(
|
||||
deviceIp,
|
||||
cmd,
|
||||
through(function (data, _enc, cb) {
|
||||
buffer.push(data.toString(enc));
|
||||
cb();
|
||||
}),
|
||||
);
|
||||
return buffer.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (hostname: string) =>
|
||||
(await getLocalDeviceCmdStdout(hostname, 'cat /etc/os-release')).toString(),
|
||||
export const getDeviceOsRelease = _.memoize(async (deviceIp: string) =>
|
||||
execBuffered(deviceIp, 'cat /etc/os-release'),
|
||||
);
|
||||
|
||||
function sshErrorMessage(cmd: string, exitSignal?: string, exitCode?: number) {
|
||||
// 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) {
|
||||
const msg: string[] = [];
|
||||
cmd = cmd ? `Remote command "${cmd}"` : 'Process';
|
||||
if (exitSignal) {
|
||||
msg.push(`SSH: ${cmd} terminated with signal "${exitSignal}"`);
|
||||
msg.push(`Warning: ssh process was terminated with signal "${exitSignal}"`);
|
||||
} else {
|
||||
msg.push(`SSH: ${cmd} exited with non-zero status code "${exitCode}"`);
|
||||
msg.push(`Warning: ssh process exited with non-zero code "${exitCode}"`);
|
||||
switch (exitCode) {
|
||||
case 255:
|
||||
msg.push(`
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2022 Balena
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -40,35 +40,14 @@ export function notify() {
|
||||
}
|
||||
}
|
||||
const up = notifier.update;
|
||||
const message = up && getNotifierMessage(up);
|
||||
if (message) {
|
||||
notifier.notify({ defer: false, message });
|
||||
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`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
@ -95,3 +95,52 @@ 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];
|
||||
}
|
||||
|
337
npm-shrinkwrap.json
generated
337
npm-shrinkwrap.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "13.7.0",
|
||||
"version": "13.1.13",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -1559,6 +1559,202 @@
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@oclif/core": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@oclif/core/-/core-1.0.10.tgz",
|
||||
"integrity": "sha512-L+IcNU3NoYxwz5hmHfcUlOJ3dpgHRsIj1kAmI9CKEJHq5gBVKlP44Ot179Jke1jKRKX2g9N42izbmlh0SNpkkw==",
|
||||
"requires": {
|
||||
"@oclif/linewrap": "^1.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"clean-stack": "^3.0.1",
|
||||
"cli-ux": "6.0.5",
|
||||
"debug": "^4.3.3",
|
||||
"fs-extra": "^9.1.0",
|
||||
"get-package-type": "^0.1.0",
|
||||
"globby": "^11.0.4",
|
||||
"indent-string": "^4.0.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"semver": "^7.3.5",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"tslib": "^2.3.1",
|
||||
"widest-line": "^3.1.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
|
||||
"requires": {
|
||||
"type-fest": "^0.21.3"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"clean-stack": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz",
|
||||
"integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==",
|
||||
"requires": {
|
||||
"escape-string-regexp": "4.0.0"
|
||||
}
|
||||
},
|
||||
"cli-progress": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.9.1.tgz",
|
||||
"integrity": "sha512-AXxiCe2a0Lm0VN+9L0jzmfQSkcZm5EYspfqXKaSIQKqIk+0hnkZ3/v1E9B39mkD6vYhKih3c/RPsJBSwq9O99Q==",
|
||||
"requires": {
|
||||
"colors": "^1.1.2",
|
||||
"string-width": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"cli-ux": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-6.0.5.tgz",
|
||||
"integrity": "sha512-q2pvzDiXMNISMqCBh0P2dkofQ/8OiWlEAjl6MDNk5oUZ6p54Fnk1rOaXxohYm+YkLX5YNUonGOrwkvuiwVreIg==",
|
||||
"requires": {
|
||||
"@oclif/core": "^1.0.8",
|
||||
"@oclif/linewrap": "^1.0.0",
|
||||
"@oclif/screen": "^1.0.4 ",
|
||||
"ansi-escapes": "^4.3.0",
|
||||
"ansi-styles": "^4.2.0",
|
||||
"cardinal": "^2.1.1",
|
||||
"chalk": "^4.1.0",
|
||||
"clean-stack": "^3.0.0",
|
||||
"cli-progress": "^3.9.1",
|
||||
"extract-stack": "^2.0.0",
|
||||
"fs-extra": "^8.1",
|
||||
"hyperlinker": "^1.0.0",
|
||||
"indent-string": "^4.0.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"natural-orderby": "^2.0.1",
|
||||
"object-treeify": "^1.1.4",
|
||||
"password-prompt": "^1.1.2",
|
||||
"semver": "^7.3.2",
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"supports-color": "^8.1.0",
|
||||
"supports-hyperlinks": "^2.1.0",
|
||||
"tslib": "^2.0.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==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||
},
|
||||
"globby": {
|
||||
"version": "11.0.4",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
|
||||
"integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
|
||||
"requires": {
|
||||
"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=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.21.3",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
||||
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="
|
||||
},
|
||||
"universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@oclif/errors": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.3.3.tgz",
|
||||
@ -2550,15 +2746,17 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.2.tgz",
|
||||
"integrity": "sha512-qC7prjoEYR2QEe6SmCVfB1x3rfcQtUr1n4x89+3e0wSTMQ/KYCyf+/RAA9n2tllkkNc6//JMUZePdFRiGIWfaQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/ssh2": "*"
|
||||
}
|
||||
},
|
||||
"@types/dockerode": {
|
||||
"version": "3.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.9.tgz",
|
||||
"integrity": "sha512-SYRN5FF/qmwpxUT6snJP5D8k0wgoUKOGVs625XvpRJOOUi6s//UYI4F0tbyE3OmzpI70Fo1+aqpzX27zCrInww==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz",
|
||||
"integrity": "sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/docker-modem": "*",
|
||||
"@types/node": "*"
|
||||
@ -2694,9 +2892,9 @@
|
||||
"integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
|
||||
},
|
||||
"@types/jsonstream": {
|
||||
"version": "0.8.30",
|
||||
@ -2944,9 +3142,12 @@
|
||||
}
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
|
||||
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ=="
|
||||
"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": "*"
|
||||
}
|
||||
},
|
||||
"@types/serve-static": {
|
||||
"version": "1.13.10",
|
||||
@ -2984,9 +3185,10 @@
|
||||
}
|
||||
},
|
||||
"@types/ssh2": {
|
||||
"version": "0.5.52",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
|
||||
"integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==",
|
||||
"version": "0.5.49",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.49.tgz",
|
||||
"integrity": "sha512-ffxhQhJqgTzrw8NxHTgkaDtAmAj2qxCyoves7ztpRgqvzbHcZTpTcm+ATWuuCbPQzxnnF4F3SGGTLGEWTZpwqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/ssh2-streams": "*"
|
||||
@ -2996,6 +3198,7 @@
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz",
|
||||
"integrity": "sha512-I2J9jKqfmvXLR5GomDiCoHrEJ58hAOmFrekfFqmCFd+A6gaEStvWnPykoWUwld1PNg4G5ag1LwdA+Lz1doRJqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@ -3808,9 +4011,9 @@
|
||||
}
|
||||
},
|
||||
"balena-preload": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-12.1.0.tgz",
|
||||
"integrity": "sha512-nsvsbYYbfkJ4kQ7Hmvq4RDw6yZp36rScNJEoZwY2xIB7Jn+OxIC5EP+XRj6tMkzvKMJYZsdCS/nbSlABxKK6ZQ==",
|
||||
"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": "^16.0.0",
|
||||
@ -3981,61 +4184,49 @@
|
||||
}
|
||||
},
|
||||
"balena-sdk": {
|
||||
"version": "16.22.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-16.22.0.tgz",
|
||||
"integrity": "sha512-HTEC8fYD0SZDlqgn0gcQ7EWliL9XIiGQ/P3f6xxvs4Nj0mG5wYtFrqK8dC6NAR0VQQxzWJtvM6iDFhshLIes7A==",
|
||||
"version": "16.9.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-16.9.0.tgz",
|
||||
"integrity": "sha512-iuIiXAEkDXoEtUJzFG5RO+rvudqMsoBppdgQLOrnIdWc14T+mvwWUFKAHHAkvxnA0GLmbxqGp7qo3uVMgS2Ojw==",
|
||||
"requires": {
|
||||
"@balena/es-version": "^1.0.0",
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@types/node": "^12.20.50",
|
||||
"@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.1.0",
|
||||
"balena-pine": "^12.4.0",
|
||||
"balena-register-device": "^7.1.0",
|
||||
"balena-request": "^11.5.5",
|
||||
"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",
|
||||
"pinejs-client-core": "^6.9.6",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
|
||||
"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.1",
|
||||
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.1.1.tgz",
|
||||
"integrity": "sha512-VpyH3py5NPMBJe1fwj5NFUeq58i2V5VaXU1EMa0ja/kUCUwTM1HL5nfNNOU3bd66V+VGqCw49iO7Wppccg3pPg==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.1.0.tgz",
|
||||
"integrity": "sha512-aLVlbdXhJi1rHpTmF9/YbheWtgAmwDUBPk3eKXhJuOZWg4XDnhbP4DUOdPBIM+U+rvXcPeBKOYqsswO0ymd96w==",
|
||||
"requires": {
|
||||
"balena-semver": "^2.0.0",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"balena-request": {
|
||||
"version": "11.5.5",
|
||||
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.5.5.tgz",
|
||||
"integrity": "sha512-sQG+OBAUKOW4KENPRGqit/34l3kWZqoT+aUdpitIG8QdKUrRjKQkjkCmDzprDEDJuXfWoCToKdleN9tYwRCXEw==",
|
||||
"requires": {
|
||||
"@balena/node-web-streams": "^0.2.3",
|
||||
"balena-errors": "^4.7.1",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"fetch-readablestream": "^0.2.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"qs": "^6.9.4",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -4576,7 +4767,7 @@
|
||||
"buffer-shims": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz",
|
||||
"integrity": "sha512-Zy8ZXMyxIT6RMTeY7OP/bDndfj6bwCan7SS98CEndS6deHwWPpseeHlwarNcBim+etXnF9HBc1non5JgDaJU1g=="
|
||||
"integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E="
|
||||
},
|
||||
"buffers": {
|
||||
"version": "0.1.1",
|
||||
@ -4922,27 +5113,26 @@
|
||||
}
|
||||
},
|
||||
"cli-ux": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-5.6.3.tgz",
|
||||
"integrity": "sha512-/oDU4v8BiDjX2OKcSunGH0iGDiEtj2rZaGyqNuv9IT4CgcSMyVWAMfn0+rEHaOc4n9ka78B0wo1+N1QX89f7mw==",
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-6.0.6.tgz",
|
||||
"integrity": "sha512-CvL4qmV78VhnbyHTswGjpDSQtU+oj3hT9DP9L6yMOwiTiNv0nMjMEV/8zou4CSqO6PtZ2A8qnlZDgAc07Js+aw==",
|
||||
"requires": {
|
||||
"@oclif/command": "^1.6.0",
|
||||
"@oclif/errors": "^1.2.1",
|
||||
"@oclif/core": "1.0.10",
|
||||
"@oclif/linewrap": "^1.0.0",
|
||||
"@oclif/screen": "^1.0.3",
|
||||
"@oclif/screen": "^1.0.4 ",
|
||||
"ansi-escapes": "^4.3.0",
|
||||
"ansi-styles": "^4.2.0",
|
||||
"cardinal": "^2.1.1",
|
||||
"chalk": "^4.1.0",
|
||||
"clean-stack": "^3.0.0",
|
||||
"cli-progress": "^3.4.0",
|
||||
"cli-progress": "^3.9.1",
|
||||
"extract-stack": "^2.0.0",
|
||||
"fs-extra": "^8.1",
|
||||
"hyperlinker": "^1.0.0",
|
||||
"indent-string": "^4.0.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash": "^4.17.21",
|
||||
"natural-orderby": "^2.0.1",
|
||||
"object-treeify": "^1.1.4",
|
||||
"password-prompt": "^1.1.2",
|
||||
@ -4963,9 +5153,9 @@
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
||||
"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"
|
||||
@ -4981,6 +5171,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli-progress": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.9.1.tgz",
|
||||
"integrity": "sha512-AXxiCe2a0Lm0VN+9L0jzmfQSkcZm5EYspfqXKaSIQKqIk+0hnkZ3/v1E9B39mkD6vYhKih3c/RPsJBSwq9O99Q==",
|
||||
"requires": {
|
||||
"colors": "^1.1.2",
|
||||
"string-width": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
@ -6178,13 +6377,12 @@
|
||||
}
|
||||
},
|
||||
"docker-progress": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/docker-progress/-/docker-progress-5.1.3.tgz",
|
||||
"integrity": "sha512-Ou+o4ISCxMvuoeG09S/irjgleLdG4ZhNiqo31p8vOzDfZm5+JaS2ouHo4lc4kXKqTN6c5eCm+62oRGoaPLDY1A==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/docker-progress/-/docker-progress-5.0.1.tgz",
|
||||
"integrity": "sha512-xVkMZNe48A6jxINzz8X/0jAy5YzAZy8u9QRK8nNSlVp1XHEhYfN0QlFZKOFzq1TO99if+c+yBpn4YZuGcZHG3Q==",
|
||||
"requires": {
|
||||
"@types/dockerode": "^3.3.9",
|
||||
"JSONStream": "^1.3.5",
|
||||
"lodash": "^4.17.21"
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"docker-qemu-transpose": {
|
||||
@ -8123,6 +8321,11 @@
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="
|
||||
},
|
||||
"get-port": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
|
||||
@ -10566,7 +10769,7 @@
|
||||
"macmount": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/macmount/-/macmount-1.0.0.tgz",
|
||||
"integrity": "sha512-kaz5wkgk4lQSAZ+Ch+TJHJHQqjmqM9TOjoLMrOp1mdLlrQBPa2qC/5Hj6OEjklVpMZn6GC2EeBibmSVeyRpXuA==",
|
||||
"integrity": "sha1-qsz7nv62fdbpRkm5HMErNtAtLPE=",
|
||||
"optional": true
|
||||
},
|
||||
"magic-string": {
|
||||
@ -17233,9 +17436,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.6.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
|
||||
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
|
||||
"integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==",
|
||||
"dev": true
|
||||
},
|
||||
"unbox-primitive": {
|
||||
@ -17396,7 +17599,7 @@
|
||||
"bluebird": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
|
||||
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
|
||||
"integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "1.0.7",
|
||||
|
17
package.json
17
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "13.7.0",
|
||||
"version": "13.1.13",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -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.9",
|
||||
"@types/dockerode": "^3.3.0",
|
||||
"@types/ejs": "^3.1.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
@ -155,7 +155,6 @@
|
||||
"@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",
|
||||
@ -191,7 +190,7 @@
|
||||
"simple-git": "^2.48.0",
|
||||
"sinon": "^11.1.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.6.4"
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
@ -207,9 +206,9 @@
|
||||
"balena-errors": "^4.7.1",
|
||||
"balena-image-fs": "^7.0.6",
|
||||
"balena-image-manager": "^7.1.1",
|
||||
"balena-preload": "^12.1.0",
|
||||
"balena-preload": "^12.0.0",
|
||||
"balena-release": "^3.2.0",
|
||||
"balena-sdk": "^16.22.0",
|
||||
"balena-sdk": "^16.9.0",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^4.0.7",
|
||||
"balena-settings-storage": "^7.0.0",
|
||||
@ -219,13 +218,13 @@
|
||||
"chalk": "^3.0.0",
|
||||
"chokidar": "^3.5.2",
|
||||
"cli-truncate": "^2.1.0",
|
||||
"cli-ux": "^5.5.1",
|
||||
"color-hash": "^1.1.1",
|
||||
"cli-ux": "^6.0.5",
|
||||
"columnify": "^1.5.2",
|
||||
"common-tags": "^1.7.2",
|
||||
"denymount": "^2.3.0",
|
||||
"docker-modem": "3.0.0",
|
||||
"docker-progress": "^5.1.3",
|
||||
"docker-progress": "^5.0.1",
|
||||
"docker-qemu-transpose": "^1.1.1",
|
||||
"dockerode": "^3.3.1",
|
||||
"ejs": "^3.1.6",
|
||||
@ -288,6 +287,6 @@
|
||||
"windosu": "^0.3.0"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2022-07-07T07:29:37.147Z"
|
||||
"publishedAt": "2022-02-10T11:50:34.458Z"
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ const johnDoe = {
|
||||
gitlab_id: 1325,
|
||||
social_service_account: null,
|
||||
hasPasswordSet: true,
|
||||
needsPasswordReset: false,
|
||||
public_key: false,
|
||||
features: [],
|
||||
id: 1344,
|
||||
@ -20,6 +21,7 @@ const janeDoe = {
|
||||
social_service_account: null,
|
||||
has_disabled_newsletter: true,
|
||||
hasPasswordSet: true,
|
||||
needsPasswordReset: false,
|
||||
public_key: false,
|
||||
features: [],
|
||||
intercomUserHash:
|
||||
|
@ -21,6 +21,8 @@ import * as path from 'path';
|
||||
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
import { isV14 } from '../../../lib/utils/version';
|
||||
|
||||
describe('balena device', function () {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
@ -57,9 +59,16 @@ describe('balena device', function () {
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
|
||||
expect(lines).to.have.lengthOf(25);
|
||||
expect(lines[0]).to.equal('== SPARKLING WOOD');
|
||||
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
|
||||
if (isV14()) {
|
||||
expect(lines).to.have.lengthOf(26);
|
||||
expect(lines[0]).to.equal('sparkling-wood');
|
||||
expect(lines[2].split(':')[0].trim()).to.equal('Id');
|
||||
expect(lines[2].split(':')[1].trim()).to.equal('1747415');
|
||||
} else {
|
||||
expect(lines).to.have.lengthOf(25);
|
||||
expect(lines[0]).to.equal('== SPARKLING WOOD');
|
||||
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
|
||||
}
|
||||
});
|
||||
|
||||
it.skip('correctly handles devices with missing fields', async () => {
|
||||
@ -79,14 +88,20 @@ describe('balena device', function () {
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
|
||||
expect(lines).to.have.lengthOf(14);
|
||||
expect(lines[0]).to.equal('== SPARKLING WOOD');
|
||||
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
|
||||
if (isV14()) {
|
||||
expect(lines).to.have.lengthOf(15);
|
||||
expect(lines[0]).to.equal('sparkling-wood');
|
||||
expect(lines[7].split(':')[1].trim()).to.equal('org/test app');
|
||||
} else {
|
||||
expect(lines).to.have.lengthOf(14);
|
||||
expect(lines[0]).to.equal('== SPARKLING WOOD');
|
||||
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
|
||||
}
|
||||
});
|
||||
|
||||
it('correctly handles devices with missing application', async () => {
|
||||
// Devices with missing applications will have application name set to `N/a`.
|
||||
// e.g. When user has a device associated with app that user is no longer a collaborator of.
|
||||
it.skip('correctly handles devices with missing fleet', async () => {
|
||||
// Devices with missing fleets will have fleet name set to `N/a`.
|
||||
// e.g. When user has a device associated with fleet that user is no longer a collaborator of.
|
||||
api.scope
|
||||
.get(
|
||||
/^\/v6\/device\?.+&\$expand=belongs_to__application\(\$select=app_name,slug\)/,
|
||||
@ -103,8 +118,15 @@ describe('balena device', function () {
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
|
||||
expect(lines).to.have.lengthOf(25);
|
||||
expect(lines[0]).to.equal('== SPARKLING WOOD');
|
||||
expect(lines[6].split(':')[1].trim()).to.equal('N/a');
|
||||
if (isV14()) {
|
||||
expect(lines).to.have.lengthOf(26);
|
||||
expect(lines[0]).to.equal('sparkling-wood');
|
||||
expect(lines[9].split(':')[0].trim()).to.equal('Fleet');
|
||||
expect(lines[9].split(':')[1].trim()).to.equal('N/a');
|
||||
} else {
|
||||
expect(lines).to.have.lengthOf(25);
|
||||
expect(lines[0]).to.equal('== SPARKLING WOOD');
|
||||
expect(lines[6].split(':')[1].trim()).to.equal('N/a');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -21,6 +21,8 @@ import * as path from 'path';
|
||||
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
import { isV14 } from '../../../lib/utils/version';
|
||||
|
||||
describe('balena devices', function () {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
@ -48,15 +50,24 @@ describe('balena devices', function () {
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
|
||||
expect(lines[0].replace(/ +/g, ' ')).to.equal(
|
||||
'ID UUID DEVICE NAME DEVICE TYPE FLEET STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
|
||||
);
|
||||
expect(lines).to.have.lengthOf.at.least(2);
|
||||
|
||||
expect(lines.some((l) => l.includes('org/test app'))).to.be.true;
|
||||
|
||||
// Devices with missing applications will have application name set to `N/a`.
|
||||
// e.g. When user has a device associated with app that user is no longer a collaborator of.
|
||||
expect(lines.some((l) => l.includes('N/a'))).to.be.true;
|
||||
if (isV14()) {
|
||||
expect(lines[0].replace(/ +/g, ' ')).to.equal(
|
||||
' Id Uuid Device name Device type Fleet Status Is online Supervisor version Os version Dashboard url ',
|
||||
);
|
||||
expect(lines).to.have.lengthOf.at.least(3);
|
||||
expect(lines.some((l) => l.includes('org/test app'))).to.be.true;
|
||||
// Devices with missing applications will have application name set to `N/a`.
|
||||
// e.g. When user has a device associated with app that user is no longer a collaborator of.
|
||||
expect(lines.some((l) => l.includes('N/a'))).to.be.true;
|
||||
} else {
|
||||
expect(lines[0].replace(/ +/g, ' ')).to.equal(
|
||||
'ID UUID DEVICE NAME DEVICE TYPE FLEET STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
|
||||
);
|
||||
expect(lines).to.have.lengthOf.at.least(2);
|
||||
expect(lines.some((l) => l.includes('org/test app'))).to.be.true;
|
||||
// Devices with missing applications will have application name set to `N/a`.
|
||||
// e.g. When user has a device associated with app that user is no longer a collaborator of.
|
||||
expect(lines.some((l) => l.includes('N/a'))).to.be.true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2021 Balena Ltd.
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,6 +19,7 @@ import { expect } from 'chai';
|
||||
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
import { isV14 } from '../../../lib/utils/version';
|
||||
|
||||
describe('balena devices supported', function () {
|
||||
let api: BalenaAPIMock;
|
||||
@ -50,7 +51,10 @@ describe('balena devices supported', function () {
|
||||
|
||||
const lines = cleanOutput(out, true);
|
||||
|
||||
expect(lines[0]).to.equal('SLUG ALIASES ARCH NAME');
|
||||
expect(lines[0]).to.equal(
|
||||
isV14() ? ' Slug Aliases Arch Name ' : 'SLUG ALIASES ARCH NAME',
|
||||
);
|
||||
|
||||
expect(lines).to.have.lengthOf.at.least(2);
|
||||
expect(lines).to.contain('intel-nuc nuc amd64 Intel NUC');
|
||||
expect(lines).to.contain(
|
||||
|
261
tests/commands/env/envs.spec.ts
vendored
261
tests/commands/env/envs.spec.ts
vendored
@ -19,7 +19,9 @@ import { expect } from 'chai';
|
||||
import { stripIndent } from '../../../build/utils/lazy';
|
||||
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { runCommand } from '../../helpers';
|
||||
import { runCommand, removeFirstNLines, trimLines } from '../../helpers';
|
||||
|
||||
import { isV14 } from '../../../lib/utils/version';
|
||||
|
||||
describe('balena envs', function () {
|
||||
const appName = 'test';
|
||||
@ -48,15 +50,30 @@ describe('balena envs', function () {
|
||||
|
||||
const { out, err } = await runCommand(`envs -f ${appName}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
if (isV14()) {
|
||||
let output = out.join('');
|
||||
output = trimLines(removeFirstNLines(output, 2));
|
||||
|
||||
const expected =
|
||||
stripIndent`
|
||||
120110 svar1 svar1-value gh_user/testApp service1
|
||||
120111 svar2 svar2-value gh_user/testApp service2
|
||||
120101 var1 var1-val gh_user/testApp *
|
||||
120102 var2 22 gh_user/testApp *
|
||||
` + '\n';
|
||||
|
||||
expect(output).to.equal(expected);
|
||||
} else {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE FLEET SERVICE
|
||||
120110 svar1 svar1-value gh_user/testApp service1
|
||||
120111 svar2 svar2-value gh_user/testApp service2
|
||||
120101 var1 var1-val gh_user/testApp *
|
||||
120102 var2 22 gh_user/testApp *
|
||||
` + '\n',
|
||||
);
|
||||
);
|
||||
}
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
@ -66,12 +83,24 @@ describe('balena envs', function () {
|
||||
|
||||
const { out, err } = await runCommand(`envs -f ${appName} --config`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
if (isV14()) {
|
||||
let output = out.join('');
|
||||
output = trimLines(removeFirstNLines(output, 2));
|
||||
|
||||
const expected =
|
||||
stripIndent`
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false gh_user/testApp
|
||||
` + '\n';
|
||||
|
||||
expect(output).to.equal(expected);
|
||||
} else {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE FLEET
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false gh_user/testApp
|
||||
` + '\n',
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
@ -82,15 +111,19 @@ describe('balena envs', function () {
|
||||
|
||||
const { out, err } = await runCommand(`envs -cjf ${appName}`);
|
||||
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal([
|
||||
{
|
||||
fleet: 'gh_user/testApp',
|
||||
id: 120300,
|
||||
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
|
||||
value: 'false',
|
||||
},
|
||||
]);
|
||||
expect(err.join('')).to.equal('');
|
||||
if (isV14()) {
|
||||
// TODO: Add tests once oclif json issue resolved.
|
||||
} else {
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal([
|
||||
{
|
||||
fleet: 'gh_user/testApp',
|
||||
id: 120300,
|
||||
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
|
||||
value: 'false',
|
||||
},
|
||||
]);
|
||||
expect(err.join('')).to.equal('');
|
||||
}
|
||||
});
|
||||
|
||||
it('should successfully list service variables for a test fleet (-s flag)', async () => {
|
||||
@ -104,14 +137,28 @@ describe('balena envs', function () {
|
||||
`envs -f ${appName} -s ${serviceName}`,
|
||||
);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
if (isV14()) {
|
||||
let output = out.join('');
|
||||
output = trimLines(removeFirstNLines(output, 2));
|
||||
|
||||
const expected =
|
||||
stripIndent`
|
||||
120111 svar2 svar2-value gh_user/testApp service2
|
||||
120101 var1 var1-val gh_user/testApp *
|
||||
120102 var2 22 gh_user/testApp *
|
||||
` + '\n';
|
||||
|
||||
expect(output).to.equal(expected);
|
||||
} else {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE FLEET SERVICE
|
||||
120111 svar2 svar2-value gh_user/testApp service2
|
||||
120101 var1 var1-val gh_user/testApp *
|
||||
120102 var2 22 gh_user/testApp *
|
||||
` + '\n',
|
||||
);
|
||||
);
|
||||
}
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
@ -126,14 +173,28 @@ describe('balena envs', function () {
|
||||
`envs -f ${appName} -s ${serviceName}`,
|
||||
);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
if (isV14()) {
|
||||
let output = out.join('');
|
||||
output = trimLines(removeFirstNLines(output, 2));
|
||||
|
||||
const expected =
|
||||
stripIndent`
|
||||
120110 svar1 svar1-value gh_user/testApp ${serviceName}
|
||||
120101 var1 var1-val gh_user/testApp *
|
||||
120102 var2 22 gh_user/testApp *
|
||||
` + '\n';
|
||||
|
||||
expect(output).to.equal(expected);
|
||||
} else {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE FLEET SERVICE
|
||||
120110 svar1 svar1-value gh_user/testApp ${serviceName}
|
||||
120101 var1 var1-val gh_user/testApp *
|
||||
120102 var2 22 gh_user/testApp *
|
||||
` + '\n',
|
||||
);
|
||||
);
|
||||
}
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
@ -148,8 +209,27 @@ describe('balena envs', function () {
|
||||
const uuid = shortUUID;
|
||||
const result = await runCommand(`envs -d ${uuid}`);
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
|
||||
if (isV14()) {
|
||||
let output = out.join('');
|
||||
output = trimLines(removeFirstNLines(output, 2));
|
||||
|
||||
const expected =
|
||||
stripIndent`
|
||||
120110 svar1 svar1-value org/test * service1
|
||||
120111 svar2 svar2-value org/test * service2
|
||||
120120 svar3 svar3-value org/test ${uuid} service1
|
||||
120121 svar4 svar4-value org/test ${uuid} service2
|
||||
120101 var1 var1-val org/test * *
|
||||
120102 var2 22 org/test * *
|
||||
120203 var3 var3-val org/test ${uuid} *
|
||||
120204 var4 44 org/test ${uuid} *
|
||||
` + '\n';
|
||||
|
||||
expect(output).to.equal(expected);
|
||||
} else {
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE FLEET DEVICE SERVICE
|
||||
120110 svar1 svar1-value org/test * service1
|
||||
120111 svar2 svar2-value org/test * service2
|
||||
@ -161,10 +241,10 @@ describe('balena envs', function () {
|
||||
120204 var4 44 org/test ${uuid} *
|
||||
` + '\n';
|
||||
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
|
||||
expect(out.join('')).to.equal(expected);
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
expect(out.join('')).to.equal(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should successfully list env variables for a test device (JSON output)', async () => {
|
||||
@ -176,7 +256,11 @@ describe('balena envs', function () {
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
|
||||
const expected = `[
|
||||
|
||||
if (isV14()) {
|
||||
// TODO: Add tests once oclif json issue resolved.
|
||||
} else {
|
||||
const expected = `[
|
||||
{ "id": 120101, "fleet": "org/test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
|
||||
{ "id": 120102, "fleet": "org/test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
|
||||
{ "id": 120110, "fleet": "org/test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" },
|
||||
@ -187,7 +271,9 @@ describe('balena envs', function () {
|
||||
{ "id": 120204, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
|
||||
]`;
|
||||
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
|
||||
}
|
||||
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
@ -199,17 +285,30 @@ describe('balena envs', function () {
|
||||
|
||||
const result = await runCommand(`envs -d ${shortUUID} --config`);
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
if (isV14()) {
|
||||
let output = out.join('');
|
||||
output = trimLines(removeFirstNLines(output, 2));
|
||||
|
||||
const expected =
|
||||
stripIndent`
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false org/test *
|
||||
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 org/test ${shortUUID}
|
||||
` + '\n';
|
||||
|
||||
expect(output).to.equal(expected);
|
||||
} else {
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE FLEET DEVICE
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false org/test *
|
||||
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 org/test ${shortUUID}
|
||||
` + '\n';
|
||||
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
|
||||
expect(out.join('')).to.equal(expected);
|
||||
expect(out.join('')).to.equal(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should successfully list service variables for a test device (-s flag)', async () => {
|
||||
@ -225,8 +324,25 @@ describe('balena envs', function () {
|
||||
const uuid = shortUUID;
|
||||
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
|
||||
if (isV14()) {
|
||||
let output = out.join('');
|
||||
output = trimLines(removeFirstNLines(output, 2));
|
||||
|
||||
const expected =
|
||||
stripIndent`
|
||||
120111 svar2 svar2-value org/test * service2
|
||||
120121 svar4 svar4-value org/test ${uuid} service2
|
||||
120101 var1 var1-val org/test * *
|
||||
120102 var2 22 org/test * *
|
||||
120203 var3 var3-val org/test ${uuid} *
|
||||
120204 var4 44 org/test ${uuid} *
|
||||
` + '\n';
|
||||
|
||||
expect(output).to.equal(expected);
|
||||
} else {
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE FLEET DEVICE SERVICE
|
||||
120111 svar2 svar2-value org/test * service2
|
||||
120121 svar4 svar4-value org/test ${uuid} service2
|
||||
@ -236,10 +352,11 @@ describe('balena envs', function () {
|
||||
120204 var4 44 org/test ${uuid} *
|
||||
` + '\n';
|
||||
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
|
||||
expect(out.join('')).to.equal(expected);
|
||||
expect(out.join('')).to.equal(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should successfully list env and service variables for a test device (unknown fleet)', async () => {
|
||||
@ -250,8 +367,23 @@ describe('balena envs', function () {
|
||||
const uuid = shortUUID;
|
||||
const result = await runCommand(`envs -d ${uuid}`);
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
|
||||
if (isV14()) {
|
||||
let output = out.join('');
|
||||
output = trimLines(removeFirstNLines(output, 2));
|
||||
|
||||
const expected =
|
||||
stripIndent`
|
||||
120120 svar3 svar3-value N/A ${uuid} service1
|
||||
120121 svar4 svar4-value N/A ${uuid} service2
|
||||
120203 var3 var3-val N/A ${uuid} *
|
||||
120204 var4 44 N/A ${uuid} *
|
||||
` + '\n';
|
||||
|
||||
expect(output).to.equal(expected);
|
||||
} else {
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE FLEET DEVICE SERVICE
|
||||
120120 svar3 svar3-value N/A ${uuid} service1
|
||||
120121 svar4 svar4-value N/A ${uuid} service2
|
||||
@ -259,10 +391,11 @@ describe('balena envs', function () {
|
||||
120204 var4 44 N/A ${uuid} *
|
||||
` + '\n';
|
||||
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
|
||||
expect(out.join('')).to.equal(expected);
|
||||
expect(out.join('')).to.equal(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should successfully list env and service vars for a test device (-s flags)', async () => {
|
||||
@ -278,8 +411,24 @@ describe('balena envs', function () {
|
||||
const uuid = shortUUID;
|
||||
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
if (isV14()) {
|
||||
let output = out.join('');
|
||||
output = trimLines(removeFirstNLines(output, 2));
|
||||
|
||||
const expected =
|
||||
stripIndent`
|
||||
120110 svar1 svar1-value org/test * ${serviceName}
|
||||
120120 svar3 svar3-value org/test ${uuid} ${serviceName}
|
||||
120101 var1 var1-val org/test * *
|
||||
120102 var2 22 org/test * *
|
||||
120203 var3 var3-val org/test ${uuid} *
|
||||
120204 var4 44 org/test ${uuid} *
|
||||
` + '\n';
|
||||
|
||||
expect(output).to.equal(expected);
|
||||
} else {
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE FLEET DEVICE SERVICE
|
||||
120110 svar1 svar1-value org/test * ${serviceName}
|
||||
120120 svar3 svar3-value org/test ${uuid} ${serviceName}
|
||||
@ -289,10 +438,11 @@ describe('balena envs', function () {
|
||||
120204 var4 44 org/test ${uuid} *
|
||||
` + '\n';
|
||||
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected.replace(/ +/g, ' ');
|
||||
|
||||
expect(out.join('')).to.equal(expected);
|
||||
expect(out.join('')).to.equal(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should successfully list env and service vars for a test device (-js flags)', async () => {
|
||||
@ -308,7 +458,11 @@ describe('balena envs', function () {
|
||||
const { out, err } = await runCommand(
|
||||
`envs -d ${shortUUID} -js ${serviceName}`,
|
||||
);
|
||||
const expected = `[
|
||||
|
||||
if (isV14()) {
|
||||
// TODO: Add tests once oclif json issue resolved.
|
||||
} else {
|
||||
const expected = `[
|
||||
{ "id": 120101, "fleet": "org/test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
|
||||
{ "id": 120102, "fleet": "org/test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
|
||||
{ "id": 120110, "fleet": "org/test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
|
||||
@ -317,7 +471,8 @@ describe('balena envs', function () {
|
||||
{ "id": 120204, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
|
||||
]`;
|
||||
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
|
||||
expect(err.join('')).to.equal('');
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
|
||||
expect(err.join('')).to.equal('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -59,7 +59,6 @@ if (process.platform !== 'win32') {
|
||||
'--config-network ethernet',
|
||||
'--initial-device-name testDeviceName',
|
||||
'--provisioning-key-name testKey',
|
||||
'--provisioning-key-expiry-date 2050-12-12',
|
||||
];
|
||||
|
||||
const { err } = await runCommand(command.join(' '));
|
||||
|
@ -20,6 +20,8 @@ import { expect } from 'chai';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../helpers';
|
||||
|
||||
import { isV14 } from '../../lib/utils/version';
|
||||
|
||||
describe('balena release', function () {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
@ -34,7 +36,7 @@ describe('balena release', function () {
|
||||
api.done();
|
||||
});
|
||||
|
||||
it('should show release details', async () => {
|
||||
it.skip('should show release details', async () => {
|
||||
api.expectGetRelease();
|
||||
const { out } = await runCommand('release 27fda508c');
|
||||
const lines = cleanOutput(out);
|
||||
@ -44,7 +46,7 @@ describe('balena release', function () {
|
||||
expect(lines[1]).to.contain(' 90247b54de4fa7a0a3cbc85e73c68039');
|
||||
});
|
||||
|
||||
it('should return release composition', async () => {
|
||||
it.skip('should return release composition', async () => {
|
||||
api.expectGetRelease();
|
||||
const { out } = await runCommand('release 27fda508c --composition');
|
||||
const lines = cleanOutput(out);
|
||||
@ -61,8 +63,14 @@ describe('balena release', function () {
|
||||
api.expectGetApplication();
|
||||
const { out } = await runCommand('releases someapp');
|
||||
const lines = cleanOutput(out);
|
||||
expect(lines.length).to.be.equal(2);
|
||||
expect(lines[1]).to.contain('142334');
|
||||
expect(lines[1]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
|
||||
if (isV14()) {
|
||||
expect(lines.length).to.be.equal(3);
|
||||
expect(lines[2]).to.contain('142334');
|
||||
expect(lines[2]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
|
||||
} else {
|
||||
expect(lines.length).to.be.equal(2);
|
||||
expect(lines[1]).to.contain('142334');
|
||||
expect(lines[1]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -32,53 +32,33 @@ 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) {
|
||||
this.skip();
|
||||
if (hasSshExecutable) {
|
||||
[sshServer, sshServerPort] = await startMockSshServer();
|
||||
}
|
||||
[sshServer, sshServerPort] = await startMockSshServer();
|
||||
await mockSpawn();
|
||||
const modPath = '../../build/utils/which';
|
||||
const mod = await import(modPath);
|
||||
mock(modPath, {
|
||||
...mod,
|
||||
whichSpawn: async () => [mockedExitCode, undefined],
|
||||
});
|
||||
});
|
||||
|
||||
this.afterAll(async function () {
|
||||
this.afterAll(function () {
|
||||
if (sshServer) {
|
||||
sshServer.close();
|
||||
sshServer = undefined;
|
||||
}
|
||||
await mockSpawn({ revert: true });
|
||||
mock.stopAll();
|
||||
});
|
||||
|
||||
this.beforeEach(function () {
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(function () {
|
||||
this.afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
});
|
||||
@ -107,7 +87,7 @@ describe('balena ssh', function () {
|
||||
async () => {
|
||||
const deviceUUID = 'abc1234';
|
||||
const expectedErrLines = [
|
||||
'SSH: Remote command "host abc1234" exited with non-zero status code "255"',
|
||||
'Warning: ssh process exited with non-zero code "255"',
|
||||
];
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetDevice({ fullUUID: deviceUUID, isOnline: true });
|
||||
@ -119,7 +99,22 @@ describe('balena ssh', function () {
|
||||
},
|
||||
);
|
||||
|
||||
itSS('should fail if device not online (mocked, device UUID)', async () => {
|
||||
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 () => {
|
||||
const deviceUUID = 'abc1234';
|
||||
const expectedErrLines = ['Device with UUID abc1234 is offline'];
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
@ -131,19 +126,6 @@ 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 */
|
||||
@ -177,7 +159,7 @@ async function startMockSshServer(): Promise<[Server, number]> {
|
||||
console.error(`mock ssh server error:\n${err}`);
|
||||
});
|
||||
|
||||
return await new Promise<[Server, number]>((resolve, reject) => {
|
||||
return 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) => {
|
||||
|
@ -106,21 +106,6 @@ describe('outputDataSet', function () {
|
||||
expect(splitHeader[1]).to.include('thing');
|
||||
});
|
||||
|
||||
/*
|
||||
it('should output fields in the order specified in `fields` param', async () => {
|
||||
const fields = ['thing_color', 'id', 'name'];
|
||||
const options = {};
|
||||
|
||||
await outputDataSet(dataSet, fields, options);
|
||||
|
||||
const headerLine = printLineSpy.firstCall.firstArg.toLowerCase();
|
||||
// split header using the `it` column as delimiter
|
||||
const splitHeader = headerLine.split('id');
|
||||
expect(splitHeader[0]).to.include('thing');
|
||||
expect(splitHeader[1]).to.include('name');
|
||||
});
|
||||
*/
|
||||
|
||||
it('should only output fields specified in `options.fields` if present', async () => {
|
||||
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||
const options = {
|
||||
@ -167,13 +152,41 @@ describe('outputDataSet', function () {
|
||||
expect(printLineSpy.getCall(0).firstArg).to.include('red');
|
||||
});
|
||||
|
||||
it(
|
||||
'should output `null` values using the provided value, ' +
|
||||
'if `options.displayNullValuesAs` is present',
|
||||
async () => {
|
||||
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||
const nullValue = 'N/a';
|
||||
const options = {
|
||||
'no-header': true,
|
||||
displayNullValuesAs: nullValue,
|
||||
};
|
||||
|
||||
const extendedDataSet = [
|
||||
...dataSet,
|
||||
{
|
||||
name: 'item3',
|
||||
id: 3,
|
||||
thing_color: null,
|
||||
thing_shape: 'round',
|
||||
},
|
||||
];
|
||||
|
||||
await outputDataSet(extendedDataSet, fields, options);
|
||||
|
||||
expect(printLineSpy.callCount).to.equal(3);
|
||||
expect(printLineSpy.getCall(2).firstArg).to.include(nullValue);
|
||||
},
|
||||
);
|
||||
|
||||
it('should output data in json format, if `options.json` true', async () => {
|
||||
const fields = ['name', 'thing_color', 'thing_shape'];
|
||||
const options = {
|
||||
json: true,
|
||||
};
|
||||
|
||||
// TODO: I've run into an oclif cli-ux bug, where numbers are output as strings in json
|
||||
// TODO: I've run into an oclif cli-ux bug, where all types (number. bool etc.) are output as strings in json
|
||||
// (this can be seen by including 'id' in the fields list above).
|
||||
// Issue opened: https://github.com/oclif/cli-ux/issues/309
|
||||
// For now removing id for this test.
|
||||
|
@ -17,8 +17,8 @@
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { getChalk } from '../lib/utils/lazy';
|
||||
|
||||
const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
|
||||
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
|
||||
@ -353,3 +353,65 @@ export async function switchSentry(
|
||||
return sentryStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to an array of character codes
|
||||
* @param text the text to convert.
|
||||
* @returns an array of character codes representing the text.
|
||||
*/
|
||||
export function stringToCharCodes(text: string) {
|
||||
return text.split('').map((c) => {
|
||||
return c.charCodeAt(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove leaving and trailing whitespace from each lime of a string.
|
||||
* @param text the text to process
|
||||
* @returns a copy of the text with the lines trimmed.
|
||||
*/
|
||||
export function trimLines(text: string) {
|
||||
let lines = text.split('\n');
|
||||
lines = lines.map((l) => l.trim());
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad each line with characters at beginning and end.
|
||||
* @param text the text to pad.
|
||||
* @param startPad the string to prepend each line with.
|
||||
* @param endPad the string to append each line with.
|
||||
* @returns a copy of the text with the specified padding.
|
||||
*/
|
||||
export function padLines(text: string, startPad: string, endPad: string = '') {
|
||||
let lines = text.split('\n');
|
||||
lines = lines.map((l) => {
|
||||
return l === '' ? '' : `${startPad}${l}${endPad}`;
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format first nLines bold.
|
||||
* @param text the text to format
|
||||
* @param nLines number of liens to format (from top)
|
||||
* @returns a copy of the text with the specified number of top lines formatted bold.
|
||||
*/
|
||||
export function boldFirstNLines(text: string, nLines: number) {
|
||||
const chalk = getChalk();
|
||||
let lines = text.split('\n');
|
||||
lines = lines.map((l, i) => {
|
||||
return i < nLines ? chalk.bold(l) : l;
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first nLines bold.
|
||||
* @param text the text to format
|
||||
* @param nLines number of liens to format (from top)
|
||||
* @returns a copy of the text with the first nLines removed.
|
||||
*/
|
||||
export function removeFirstNLines(text: string, nLines: number) {
|
||||
return text.split('\n').slice(nLines).join(`\n`);
|
||||
}
|
||||
|
@ -1,137 +0,0 @@
|
||||
/**
|
||||
* @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,
|
||||
});
|
||||
});
|
||||
});
|
@ -1,79 +0,0 @@
|
||||
/**
|
||||
* @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('');
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user