mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
142 Commits
v12.55.0
...
custom-doc
Author | SHA1 | Date | |
---|---|---|---|
7f8106a64b | |||
ba3a3865b5 | |||
f8402bc40c | |||
c667ffa8eb | |||
6d6065ddf5 | |||
44f55f8e7b | |||
d2c77760b3 | |||
7496710c85 | |||
be6a468507 | |||
88835e63bd | |||
3572cb3cd6 | |||
7fbd1de063 | |||
a4ab07cd08 | |||
9185eaa2b7 | |||
ff3abe1fba | |||
1ac3b70b81 | |||
e946178953 | |||
6589589bee | |||
6ae598b55e | |||
915f7e3763 | |||
cd17d79067 | |||
7e4f4392e9 | |||
3c0e998616 | |||
bd1bf8153d | |||
f2528dcd18 | |||
ec26433925 | |||
43cddd2e5d | |||
eeb2be2912 | |||
3bf8befb1d | |||
948095ce4d | |||
d2330f9ed1 | |||
cc19b00998 | |||
ed5ac75a10 | |||
465b8a1b5e | |||
eccadbdcb9 | |||
31eb734af1 | |||
fa7b59d64f | |||
1e42bfa0d5 | |||
5464e550e7 | |||
c0f27a663d | |||
d1c61c62ab | |||
a9691bff57 | |||
f5d09a43cd | |||
d11e547e11 | |||
bd462aee02 | |||
f633c0468b | |||
e4f61a1242 | |||
96142a002e | |||
6b9a5cd89c | |||
ba2d3d60ec | |||
d1e66bc1a5 | |||
58799915a9 | |||
5f2d55f569 | |||
8d6e51391c | |||
8454b02988 | |||
879d98ef98 | |||
c4e317a290 | |||
7ca4d2d720 | |||
e1e88ec56d | |||
33f7fa3829 | |||
3d516e7c5f | |||
a8507508b7 | |||
008972b3d3 | |||
92b86330a0 | |||
2563c07c6a | |||
1d4b949cf3 | |||
d17e02a930 | |||
a355cbaa79 | |||
bd021c0a2d | |||
a80f676804 | |||
f723c58089 | |||
e27a4e2e31 | |||
b91b72c408 | |||
5cf84d3f1d | |||
7d58b8c120 | |||
851301a336 | |||
ec6fd050f6 | |||
6f81053882 | |||
dbd8a9a08c | |||
256f1abf1b | |||
acd352cb3c | |||
31f927c27c | |||
3d0f16168a | |||
b2d932afab | |||
398175f0b3 | |||
2fb9c6c773 | |||
66608b32e9 | |||
c403683edf | |||
1e6ab46ca3 | |||
02d3220f2d | |||
c86cdc8f84 | |||
84f02dc063 | |||
9145f2fb28 | |||
1164388d78 | |||
06f6094401 | |||
67e11467f7 | |||
c8dfd0ca65 | |||
8b110a835a | |||
7564d95f82 | |||
f12f2b79ef | |||
176d731f9e | |||
1ed39d1d37 | |||
580ca0d584 | |||
73572df7cf | |||
23b42b1a2b | |||
632322e3c2 | |||
4faa5d7f57 | |||
9b967592a9 | |||
e01483cd2b | |||
6d89ff4bbf | |||
126e731117 | |||
32d26ad074 | |||
2bcfec9d0f | |||
c04e63ab7d | |||
79be06820c | |||
ffb94c380f | |||
385b5e9ec6 | |||
8d3a4343cb | |||
6eeb16245b | |||
3961060f90 | |||
a6dfc9126a | |||
e7ddd07b7b | |||
fea351d960 | |||
40e0b2dbed | |||
3def4d0e4a | |||
aa286cc0e7 | |||
8abeb6aed7 | |||
f285880135 | |||
2b5c387313 | |||
8babf4c908 | |||
bfc995e948 | |||
c6a0bc0fba | |||
ae69accf0f | |||
cfcace4c99 | |||
6e07db0813 | |||
5c40c8d51f | |||
d827005154 | |||
76081343cc | |||
f3fb9b6bdf | |||
c125e0b38d | |||
73b2f6b4b1 | |||
fdc0d08e96 |
17
.resinci.yml
17
.resinci.yml
@ -7,9 +7,14 @@ npm:
|
||||
node_versions:
|
||||
- "12"
|
||||
- "14"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "12"
|
||||
- "14"
|
||||
##
|
||||
## Temporarily skip Alpine tests until the following issues are resolved:
|
||||
## * https://github.com/concourse/concourse/issues/7905
|
||||
## * https://github.com/product-os/balena-concourse/issues/631
|
||||
##
|
||||
# - name: linux
|
||||
# os: alpine
|
||||
# architecture: x86_64
|
||||
# node_versions:
|
||||
# - "12"
|
||||
# - "14"
|
||||
|
File diff suppressed because it is too large
Load Diff
601
CHANGELOG.md
601
CHANGELOG.md
@ -4,6 +4,607 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 13.4.1 - 2022-04-11
|
||||
|
||||
* leave: Update log message to advise that device still needs deleting [Taro Murao]
|
||||
|
||||
## 13.4.0 - 2022-04-08
|
||||
|
||||
* deploy: Support all valid semver versions in balena.yml [Thodoris Greasidis]
|
||||
|
||||
## 13.3.3 - 2022-04-08
|
||||
|
||||
* Document the 'patches' folder in CONTRIBUTING.md [Paulo Castro]
|
||||
|
||||
## 13.3.2 - 2022-04-07
|
||||
|
||||
* Skip Alpine tests until Concourse + Alpine v3.14 issues are resolved [Paulo Castro]
|
||||
* build: Ensure HTTPS is used with dockerPort 2376 or with ca/cert/key [Paulo Castro]
|
||||
|
||||
## 13.3.1 - 2022-03-08
|
||||
|
||||
* Include link to Wiki release notes in version update notifications [Paulo Castro]
|
||||
|
||||
## 13.3.0 - 2022-02-28
|
||||
|
||||
* ssh: Allow ssh to service with IP address and production balenaOS image [Paulo Castro]
|
||||
* ssh: Advise use of 'balena login' if root authentication fails [Paulo Castro]
|
||||
|
||||
## 13.2.1 - 2022-02-24
|
||||
|
||||
* Remove unnecessary fetch of device info in `balena tunnel` [Pagan Gazzard]
|
||||
* Correctly use the device uuid when logging the tunnel target [Pagan Gazzard]
|
||||
|
||||
## 13.2.0 - 2022-02-12
|
||||
|
||||
* ssh: Attempt cloud username if 'root' authentication fails [Paulo Castro]
|
||||
* Replace occurrence of through2 dependency with standard stream module [Paulo Castro]
|
||||
* Refactor cached username logic from events.ts to bootstrap.ts for reuse [Paulo Castro]
|
||||
|
||||
## 13.1.13 - 2022-02-10
|
||||
|
||||
* Drop unused awaitDevice utility function [Lucian Buzzo]
|
||||
|
||||
## 13.1.12 - 2022-02-09
|
||||
|
||||
|
||||
<details>
|
||||
<summary> preload: Stop using the deprecated /device-types/v1 API endpoints [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-preload-12.0.0 - 2022-01-27
|
||||
>
|
||||
> * Improve types [Thodoris Greasidis]
|
||||
> * Stop relying on the /device-types/v1 endpoints [Thodoris Greasidis]
|
||||
> * Bump TypeScript to v4.5 [Thodoris Greasidis]
|
||||
>
|
||||
> <details>
|
||||
> <summary> Bump balena-sdk to v16 [Thodoris Greasidis] </summary>
|
||||
>
|
||||
>> #### balena-sdk-16.0.0 - 2021-11-28
|
||||
>>
|
||||
>> * **BREAKING**: Merge the hostApp model into the OS model [Thodoris Greasidis]
|
||||
>> * **BREAKING** Drop os.getSupportedVersions() method in favor of hostapp.getAvailableOsVersions() [Thodoris Greasidis]
|
||||
>> * os.getMaxSatisfyingVersion: Add optional param to choose OS line type [Thodoris Greasidis]
|
||||
>> * os.getMaxSatisfyingVersion: Include ESR versions [Thodoris Greasidis]
|
||||
>> * os.getMaxSatisfyingVersion: Switch to use hostApps [Thodoris Greasidis]
|
||||
>> * hostapp.getAvailableOsVersions: Add single device type argument overload [Thodoris Greasidis]
|
||||
>> * hostapp.getAllOsVersions: Add single device type argument overload [Thodoris Greasidis]
|
||||
>> * models.hostapp: Add a getAvailableOsVersions() convenience method [Thodoris Greasidis]
|
||||
>> * Support optional extra PineOptions in hostapp.getAllOsVersions() [Thodoris Greasidis]
|
||||
>> * **BREAKING** Include invalidated versions in hostapp.getAllOsVersions() [Thodoris Greasidis]
|
||||
>> * models/application: Add getDirectlyAccessible & getAllDirectlyAccessible [Thodoris Greasidis]
|
||||
>> * application.get: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
|
||||
>> * application.getAll: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
|
||||
>> * **BREAKING** Change application.getAll to include public apps [Thodoris Greasidis]
|
||||
>> * **BREAKING** Drop targeting/retrieving apps by name in favor of slugs [Thodoris Greasidis]
|
||||
>> * Bump minimum supported Typescript to v4.5.2 [Thodoris Greasidis]
|
||||
>> * **BREAKING**: Stop actively supporting node 10 [Thodoris Greasidis]
|
||||
>> * **BREAKING** Drop application.getAllWithDeviceServiceDetails() [Thodoris Greasidis]
|
||||
>> * **BREAKING** Change apiKey.getAll() to return all key variants [Thodoris Greasidis]
|
||||
>> * types: Drop is_in_local_mode from the Device model [Thodoris Greasidis]
|
||||
>> * types: Drop user__is_member_of__application in favor of the term form [Thodoris Greasidis]
|
||||
>> * typings: Drop Subscription's discounts__plan_addon property [Thodoris Greasidis]
|
||||
>> * typings: Stop extending the JWTUser type in the User model [Thodoris Greasidis]
|
||||
>> * models/config: Change the BETA device type state to NEW [Thodoris Greasidis]
|
||||
>> * typings: Drop the PineWithSelectOnGet type [Thodoris Greasidis]
|
||||
>> * Remove my_application from the supported resources [Thodoris Greasidis]
|
||||
>> * typings: Properly type some Device properties [Thodoris Greasidis]
|
||||
>> * typings: Drop the DeviceWithImageInstalls type [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.59.2 - 2021-11-28
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update balena-request to 11.5.0 [Thodoris Greasidis] </summary>
|
||||
>>
|
||||
>>> ##### balena-request-11.5.0 - 2021-11-28
|
||||
>>>
|
||||
>>> * Convert tests to JavaScript and drop coffeescript [Thodoris Greasidis]
|
||||
>>> * Fix the jsdoc generation [Thodoris Greasidis]
|
||||
>>> * Convert to typescript and publish typings [Thodoris Greasidis]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>>
|
||||
>> #### balena-sdk-15.59.1 - 2021-11-28
|
||||
>>
|
||||
>> * Fix the typings of the Image contract field [Thodoris Greasidis]
|
||||
>> * Fix the typings for the Release contract field [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.59.0 - 2021-11-24
|
||||
>>
|
||||
>> * Add release setIsInvalidated function [Matthew Yarmolinsky]
|
||||
>>
|
||||
>> #### balena-sdk-15.58.1 - 2021-11-17
|
||||
>>
|
||||
>> * Update typescript to 4.5.2 [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.58.0 - 2021-11-16
|
||||
>>
|
||||
>> * models/release: Add note() method [Thodoris Greasidis]
|
||||
>> * typings: Add the release.invalidation_reason property [Thodoris Greasidis]
|
||||
>> * typings: Add the release.note property [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.57.2 - 2021-11-15
|
||||
>>
|
||||
>> * tests/logs: Increase the wait time for retrieving the subscribed logs [Thodoris Greasidis]
|
||||
>> * tests/logs: Refactor to async-await [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.57.1 - 2021-11-11
|
||||
>>
|
||||
>> * typings: Fix $filters for resources with non numeric ids [Thodoris Greasidis]
|
||||
>> * typings: Add application.can_use__application_as_host ReverseNavigation [Thodoris Greasidis]
|
||||
>> * Add missing apiKey.getDeviceApiKeysByDevice docs [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.57.0 - 2021-11-05
|
||||
>>
|
||||
>> * models/api-key: Change update() & revoke() to work with all key variants [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.56.0 - 2021-11-04
|
||||
>>
|
||||
>> * models/apiKey: Add getDeviceApiKeysByDevice() method [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.55.0 - 2021-11-01
|
||||
>>
|
||||
>> * typings: Add the release.raw_version property [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.54.2 - 2021-10-25
|
||||
>>
|
||||
>> * application/create: Rely on the hostApps for detecting discontinued DTs [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.54.1 - 2021-10-22
|
||||
>>
|
||||
>> * tests/device: Async-await conversions & abstraction on multi-field tests [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.54.0 - 2021-10-20
|
||||
>>
|
||||
>> * tests: Register devices in chunks of 10 to avoid uuid conflicts in node [Thodoris Greasidis]
|
||||
>> * Add known issue check on release isReccomanded logic [JSReds]
|
||||
>> * Add known_issue_list to hostApp.getOsVersions() [JSReds]
|
||||
>>
|
||||
>> #### balena-sdk-15.53.0 - 2021-10-07
|
||||
>>
|
||||
>> * Add support for batch device supervisor updates [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.52.0 - 2021-10-06
|
||||
>>
|
||||
>> * Add support for batch device pinning to release [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.51.4 - 2021-09-28
|
||||
>>
|
||||
>> * auth.isLoggedIn: Treat BalenaExpiredToken errors as logged out indicator [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.51.3 - 2021-09-28
|
||||
>>
|
||||
>> * Convert application spec to TypeScript [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.51.2 - 2021-09-28
|
||||
>>
|
||||
>> * application.trackLatestRelease: Fix using draft/invalidated releases [Thodoris Greasidis]
|
||||
>> * application.isTrackingLatestRelease: Exclude draft&invalidated releases [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.51.1 - 2021-09-20
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update balena-request to v11.4.2 [Kyle Harding] </summary>
|
||||
>>
|
||||
>>> ##### balena-request-11.4.2 - 2021-09-20
|
||||
>>>
|
||||
>>> * Allow overriding the default zlib flush setting [Kyle Harding]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>>
|
||||
>> #### balena-sdk-15.51.0 - 2021-09-16
|
||||
>>
|
||||
>> * os.getConfig: Add typings for the provisioningKeyName option [Nitish Agarwal]
|
||||
>>
|
||||
>> #### balena-sdk-15.50.1 - 2021-09-13
|
||||
>>
|
||||
>> * models/os: Always first normalize the device type slug [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.50.0 - 2021-09-10
|
||||
>>
|
||||
>> * Add release.finalize to promote draft releases to final [toochevere]
|
||||
>>
|
||||
>> #### balena-sdk-15.49.1 - 2021-09-10
|
||||
>>
|
||||
>> * typings: Drop the v5-model-only application_type.is_host_os [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.49.0 - 2021-09-06
|
||||
>>
|
||||
>> * os.getSupportedOsUpdateVersions: Use the hostApp releases [Thodoris Greasidis]
|
||||
>> * os.download: Use the hostApp for finding the latest release [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.48.3 - 2021-08-27
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update balena-request to 11.4.1 [Kyle Harding] </summary>
|
||||
>>
|
||||
>>> ##### balena-request-11.4.1 - 2021-08-27
|
||||
>>>
|
||||
>>> * Allow more lenient gzip decompression [Kyle Harding]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>>
|
||||
>> #### balena-sdk-15.48.2 - 2021-08-27
|
||||
>>
|
||||
>> * Improve hostapp.getAllOsVersions performance & reduce fetched data [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.48.1 - 2021-08-27
|
||||
>>
|
||||
>> * Update typescript to 4.4.2 [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.48.0 - 2021-08-15
|
||||
>>
|
||||
>> * Deprecate the release.release_version property [Thodoris Greasidis]
|
||||
>> * typings: Add the release versioning properties [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.47.1 - 2021-08-10
|
||||
>>
|
||||
>> * Run browser tests using the minified browser bundle [Thodoris Greasidis]
|
||||
>> * Move to uglify-js to fix const assignment bug in minified build [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.47.0 - 2021-08-09
|
||||
>>
|
||||
>> * typings: Add the release.is_final & is_finalized_at__date properties [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.46.1 - 2021-07-28
|
||||
>>
|
||||
>> * apiKey.getAll: Return only NamedUserApiKeys for backwards compatibility [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.46.0 - 2021-07-27
|
||||
>>
|
||||
>> * Add email verification & email request methods [Nitish Agarwal]
|
||||
>>
|
||||
>> #### balena-sdk-15.45.0 - 2021-07-26
|
||||
>>
|
||||
>> * Update generateProvisioningKey to include keyName [Nitish Agarwal]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
</details>
|
||||
|
||||
## 13.1.11 - 2022-01-19
|
||||
|
||||
* chore: lib/auth/utils.ts: Replace deprecated url.resolve, use async/await [Paulo Castro]
|
||||
* chore: Update @types/node to v12.20.42 [Paulo Castro]
|
||||
|
||||
## 13.1.10 - 2022-01-16
|
||||
|
||||
* Update docs and package.json re min Node.js supported version (12.8.0) [Paulo Castro]
|
||||
|
||||
## 13.1.9 - 2022-01-14
|
||||
|
||||
* Update packages in response to `colors` package issues [Scott Lowe]
|
||||
|
||||
## 13.1.8 - 2022-01-11
|
||||
|
||||
* local push: Fix "invalid character '/' looking for beginning of value" [Paulo Castro]
|
||||
* v14 preparations: Fix TypeError produced by 'npx oclif manifest' [Paulo Castro]
|
||||
|
||||
## 13.1.7 - 2022-01-06
|
||||
|
||||
* Update to pkg 5 [Pagan Gazzard]
|
||||
|
||||
## 13.1.6 - 2022-01-04
|
||||
|
||||
* Automation: enforce noImplicitAny for the type-checked javascript [Pagan Gazzard]
|
||||
|
||||
## 13.1.5 - 2022-01-04
|
||||
|
||||
* Build: switch from using inline-source via gulp to using it directly [Pagan Gazzard]
|
||||
|
||||
## 13.1.4 - 2022-01-03
|
||||
|
||||
* Update pkg [Pagan Gazzard]
|
||||
|
||||
## 13.1.3 - 2022-01-03
|
||||
|
||||
* Convert lib/utils/deploy-legacy to typescript [Pagan Gazzard]
|
||||
|
||||
## 13.1.2 - 2022-01-03
|
||||
|
||||
* Convert lib/utils/compose to typescript [Pagan Gazzard]
|
||||
|
||||
## 13.1.1 - 2021-12-30
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependencies [Pagan Gazzard] </summary>
|
||||
|
||||
> ### docker-progress-5.0.1 - 2021-09-22
|
||||
>
|
||||
> * Fix for bad progress values from some registries causing a crash [Paul Jonathan Zoulin]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 13.1.0 - 2021-12-29
|
||||
|
||||
* os configure, config generate: Add '--dev' option for OS developmentMode [Paulo Castro]
|
||||
* local configure: Allow configuring 'developmentMode' in config.json [Paulo Castro]
|
||||
* os build-config: Clarify command purpose in help output [Paulo Castro]
|
||||
|
||||
<details>
|
||||
<summary> device os-update: Add support for unified dev/prod balenaOS releases [Paulo Castro] </summary>
|
||||
|
||||
> ### balena-sdk-16.9.0 - 2021-12-24
|
||||
>
|
||||
> * Support upgrading .dev to unified OS releases [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.8.2 - 2021-12-24
|
||||
>
|
||||
> * tests: Stop using mochainon [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 13.0.2 - 2021-12-24
|
||||
|
||||
* Update oclif [Pagan Gazzard]
|
||||
|
||||
## 13.0.1 - 2021-12-24
|
||||
|
||||
* os versions, os download: Replace deprecated version fields [Paulo Castro]
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to v16.8.1 [Paulo Castro] </summary>
|
||||
|
||||
> ### balena-sdk-16.8.1 - 2021-12-23
|
||||
>
|
||||
> * logs: Stop using the querystring module [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 13.0.0 - 2021-12-23
|
||||
|
||||
* v13 RELEASE NOTES: see https://git.io/JDHxG [Paulo Castro]
|
||||
* balena SDK v16: Ensure all SDK calls use fleet slug rather than name [Paulo Castro]
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to v16.8.0 [Paulo Castro] </summary>
|
||||
|
||||
> ### balena-sdk-16.8.0 - 2021-12-22
|
||||
>
|
||||
> * os.getConfig: Accept additional developmentMode configuration option [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.7.0 - 2021-12-22
|
||||
>
|
||||
> * os.download: Fix the inferred method typings [Thodoris Greasidis]
|
||||
> * os.download: Accept additional configuration options [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.6.0 - 2021-12-22
|
||||
>
|
||||
> * models.os: Use the native hostApp OS release version if it is set [Thodoris Greasidis]
|
||||
> * models.os: Deprecate OsVersion.rawVersion in favor or raw_version [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.5.0 - 2021-12-22
|
||||
>
|
||||
> * os.getAllOsVersions: Add support for invariant OS releases [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.4.1 - 2021-12-21
|
||||
>
|
||||
> * os.getMaxSatisfyingVersion: Add ">" semver range tests [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.4.0 - 2021-12-21
|
||||
>
|
||||
> * os.getMaxSatisfyingVersion: Add support for ESR releases [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.3.0 - 2021-12-21
|
||||
>
|
||||
> * application.getAppByName: Add 'directly_accessible' convenience filter [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.2.3 - 2021-12-17
|
||||
>
|
||||
> * FIx the return type of config.getConfigVarSchema() [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.2.2 - 2021-12-17
|
||||
>
|
||||
> * os.getAvailableOsVersions: Exclude draft and non-successful releases [Thodoris Greasidis]
|
||||
> * os.getAllOsVersions: Deprecate OsVersion.isRecommended [Thodoris Greasidis]
|
||||
> * os.getAllOsVersions: Deprecate OsVersion.formattedVersion [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.2.1 - 2021-12-17
|
||||
>
|
||||
> * Drop require-npm4-to-publish [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.2.0 - 2021-12-17
|
||||
>
|
||||
> * minor: Add Configuration Variables Schema method [Vipul Gupta (@vipulgupta2048)]
|
||||
>
|
||||
> ### balena-sdk-16.1.0 - 2021-12-08
|
||||
>
|
||||
> * Add description field to generateProvisioningKey for apps. [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-16.0.0 - 2021-11-28
|
||||
>
|
||||
> * **BREAKING**: Merge the hostApp model into the OS model [Thodoris Greasidis]
|
||||
> * **BREAKING** Drop os.getSupportedVersions() method in favor of hostapp.getAvailableOsVersions() [Thodoris Greasidis]
|
||||
> * os.getMaxSatisfyingVersion: Add optional param to choose OS line type [Thodoris Greasidis]
|
||||
> * os.getMaxSatisfyingVersion: Include ESR versions [Thodoris Greasidis]
|
||||
> * os.getMaxSatisfyingVersion: Switch to use hostApps [Thodoris Greasidis]
|
||||
> * hostapp.getAvailableOsVersions: Add single device type argument overload [Thodoris Greasidis]
|
||||
> * hostapp.getAllOsVersions: Add single device type argument overload [Thodoris Greasidis]
|
||||
> * models.hostapp: Add a getAvailableOsVersions() convenience method [Thodoris Greasidis]
|
||||
> * Support optional extra PineOptions in hostapp.getAllOsVersions() [Thodoris Greasidis]
|
||||
> * **BREAKING** Include invalidated versions in hostapp.getAllOsVersions() [Thodoris Greasidis]
|
||||
> * models/application: Add getDirectlyAccessible & getAllDirectlyAccessible [Thodoris Greasidis]
|
||||
> * application.get: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
|
||||
> * application.getAll: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
|
||||
> * **BREAKING** Change application.getAll to include public apps [Thodoris Greasidis]
|
||||
> * **BREAKING** Drop targeting/retrieving apps by name in favor of slugs [Thodoris Greasidis]
|
||||
> * Bump minimum supported Typescript to v4.5.2 [Thodoris Greasidis]
|
||||
> * **BREAKING**: Stop actively supporting node 10 [Thodoris Greasidis]
|
||||
> * **BREAKING** Drop application.getAllWithDeviceServiceDetails() [Thodoris Greasidis]
|
||||
> * **BREAKING** Change apiKey.getAll() to return all key variants [Thodoris Greasidis]
|
||||
> * types: Drop is_in_local_mode from the Device model [Thodoris Greasidis]
|
||||
> * types: Drop user__is_member_of__application in favor of the term form [Thodoris Greasidis]
|
||||
> * typings: Drop Subscription's discounts__plan_addon property [Thodoris Greasidis]
|
||||
> * typings: Stop extending the JWTUser type in the User model [Thodoris Greasidis]
|
||||
> * models/config: Change the BETA device type state to NEW [Thodoris Greasidis]
|
||||
> * typings: Drop the PineWithSelectOnGet type [Thodoris Greasidis]
|
||||
> * Remove my_application from the supported resources [Thodoris Greasidis]
|
||||
> * typings: Properly type some Device properties [Thodoris Greasidis]
|
||||
> * typings: Drop the DeviceWithImageInstalls type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.59.2 - 2021-11-28
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update balena-request to 11.5.0 [Thodoris Greasidis] </summary>
|
||||
>
|
||||
>> #### balena-request-11.5.0 - 2021-11-28
|
||||
>>
|
||||
>> * Convert tests to JavaScript and drop coffeescript [Thodoris Greasidis]
|
||||
>> * Fix the jsdoc generation [Thodoris Greasidis]
|
||||
>> * Convert to typescript and publish typings [Thodoris Greasidis]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-sdk-15.59.1 - 2021-11-28
|
||||
>
|
||||
> * Fix the typings of the Image contract field [Thodoris Greasidis]
|
||||
> * Fix the typings for the Release contract field [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.59.0 - 2021-11-24
|
||||
>
|
||||
> * Add release setIsInvalidated function [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-15.58.1 - 2021-11-17
|
||||
>
|
||||
> * Update typescript to 4.5.2 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.58.0 - 2021-11-16
|
||||
>
|
||||
> * models/release: Add note() method [Thodoris Greasidis]
|
||||
> * typings: Add the release.invalidation_reason property [Thodoris Greasidis]
|
||||
> * typings: Add the release.note property [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.57.2 - 2021-11-15
|
||||
>
|
||||
> * tests/logs: Increase the wait time for retrieving the subscribed logs [Thodoris Greasidis]
|
||||
> * tests/logs: Refactor to async-await [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.57.1 - 2021-11-11
|
||||
>
|
||||
> * typings: Fix $filters for resources with non numeric ids [Thodoris Greasidis]
|
||||
> * typings: Add application.can_use__application_as_host ReverseNavigation [Thodoris Greasidis]
|
||||
> * Add missing apiKey.getDeviceApiKeysByDevice docs [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.57.0 - 2021-11-05
|
||||
>
|
||||
> * models/api-key: Change update() & revoke() to work with all key variants [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.56.0 - 2021-11-04
|
||||
>
|
||||
> * models/apiKey: Add getDeviceApiKeysByDevice() method [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.55.0 - 2021-11-01
|
||||
>
|
||||
> * typings: Add the release.raw_version property [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.54.2 - 2021-10-25
|
||||
>
|
||||
> * application/create: Rely on the hostApps for detecting discontinued DTs [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.54.1 - 2021-10-22
|
||||
>
|
||||
> * tests/device: Async-await conversions & abstraction on multi-field tests [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.54.0 - 2021-10-20
|
||||
>
|
||||
> * tests: Register devices in chunks of 10 to avoid uuid conflicts in node [Thodoris Greasidis]
|
||||
> * Add known issue check on release isReccomanded logic [JSReds]
|
||||
> * Add known_issue_list to hostApp.getOsVersions() [JSReds]
|
||||
>
|
||||
> ### balena-sdk-15.53.0 - 2021-10-07
|
||||
>
|
||||
> * Add support for batch device supervisor updates [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.52.0 - 2021-10-06
|
||||
>
|
||||
> * Add support for batch device pinning to release [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.51.4 - 2021-09-28
|
||||
>
|
||||
> * auth.isLoggedIn: Treat BalenaExpiredToken errors as logged out indicator [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.51.3 - 2021-09-28
|
||||
>
|
||||
> * Convert application spec to TypeScript [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.51.2 - 2021-09-28
|
||||
>
|
||||
> * application.trackLatestRelease: Fix using draft/invalidated releases [Thodoris Greasidis]
|
||||
> * application.isTrackingLatestRelease: Exclude draft&invalidated releases [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
* device, devices: Print the fleet's slug in 'org/fleetName' format [Paulo Castro]
|
||||
* envs: Print the fleet's slug in 'org/fleetName' format [Paulo Castro]
|
||||
* os configure: Remove deprecated '--device-api-key' option [Paulo Castro]
|
||||
* Clean up unused v13 feature switch code [Paulo Castro]
|
||||
* config read/write/inject/reconfigure: Place '--type' option behind v14 switch [Paulo Castro]
|
||||
* fleet create: Don't print fleet's numeric database ID in confirmation msg [Paulo Castro]
|
||||
* devices supported: Remove deprecated '--verbose' and '--discontinued' options [Paulo Castro]
|
||||
* build/deploy/push: Remove deprecated '--convert-eol' option [Paulo Castro]
|
||||
* Move some v13 features behind v14 switch. [Scott Lowe]
|
||||
* Remove deprecated '--app' and '--application' options (renamed to '--fleet') [Paulo Castro]
|
||||
* Remove deprecated commands 'app' and 'apps' (renamed to 'fleet' and 'fleets') [Paulo Castro]
|
||||
* build/deploy/push: Remove deprecated '--[no]gitignore' option [Paulo Castro]
|
||||
* v13 release: Flip the v13 feature switch [Paulo Castro]
|
||||
* v13 release: Drop support for Node.js v10 (package.json engines.node) [Paulo Castro]
|
||||
|
||||
## 12.55.11 - 2021-12-23
|
||||
|
||||
* Update to typescript 4.5 [Pagan Gazzard]
|
||||
|
||||
## 12.55.10 - 2021-12-23
|
||||
|
||||
* Update dev dependencies [Pagan Gazzard]
|
||||
|
||||
## 12.55.9 - 2021-12-22
|
||||
|
||||
* os download: Future-proof '--version' format for unified dev/prod variants [Paulo Castro]
|
||||
|
||||
## 12.55.8 - 2021-12-21
|
||||
|
||||
* Include version info when installed on windows [Pagan Gazzard]
|
||||
* Switch from the deprecated oclif-dev commands to the oclif commands [Pagan Gazzard]
|
||||
|
||||
## 12.55.7 - 2021-12-14
|
||||
|
||||
* push: Remove hardcoded 'balenaCloud' in console message [Pranav Peshwe]
|
||||
|
||||
## 12.55.6 - 2021-12-14
|
||||
|
||||
* Fix symbolic link regression in push & deploy [Thodoris Greasidis]
|
||||
|
||||
## 12.55.5 - 2021-12-13
|
||||
|
||||
* Drop unnecessary directory list created during balena deploy & push [Thodoris Greasidis]
|
||||
|
||||
## 12.55.4 - 2021-12-10
|
||||
|
||||
* os download, os versions: Accept device type aliases [Paulo Castro]
|
||||
|
||||
## 12.55.3 - 2021-12-09
|
||||
|
||||
* os download: Improve error message when not logged in (balenaOS ESR versions) [Paulo Castro]
|
||||
* os download: Assume '.prod' suffix by default for all balenaOS versions [Paulo Castro]
|
||||
|
||||
## 12.55.2 - 2021-12-08
|
||||
|
||||
* v13 preparations: Standardize command data output [Scott Lowe]
|
||||
|
||||
## 12.55.1 - 2021-11-30
|
||||
|
||||
* chore: Bump multicast-dns to rebased commit [Kyle Harding]
|
||||
|
||||
## 12.55.0 - 2021-11-29
|
||||
|
||||
* Add provisioning key name option to config generate options [Nitish Agarwal]
|
||||
|
@ -125,6 +125,39 @@ The README file is manually edited, but subsections are automatically extracted
|
||||
|
||||
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
|
||||
## Patches folder
|
||||
|
||||
The `patches` folder contains patch files created with the
|
||||
[patch-package](https://www.npmjs.com/package/patch-package) tool. Small code changes to
|
||||
third-party modules can be made by directly editing Javascript files under the `node_modules`
|
||||
folder and then running `patch-package` to create the patch files. The patch files are then
|
||||
applied immediately after `npm install`, through the `postinstall` script defined in
|
||||
`package.json`.
|
||||
|
||||
The subfolders of the `patches` folder are documented in the
|
||||
[apply-patches.js](https://github.com/balena-io/balena-cli/blob/master/patches/apply-patches.js)
|
||||
script.
|
||||
|
||||
To make changes to the patch files under the `patches` folder, **do not edit them directly,**
|
||||
not even for a "single character change" because the hash values in the patch files also need
|
||||
to be recomputed by `patch-packages`. Instead, edit the relevant files under `node_modules`
|
||||
directly, and then run `patch-packages` with the `--patch-dir` option to specify the subfolder
|
||||
where the patch should be saved. For example, edit `node_modules/exit-hook/index.js` and then
|
||||
run:
|
||||
|
||||
```sh
|
||||
$ npx patch-package --patch-dir patches/all exit-hook
|
||||
```
|
||||
|
||||
That said, these kinds of patches should be avoided in favour of creating pull requests
|
||||
upstream. Patch files create additional maintenance work over time as the patches need to be
|
||||
updated when the dependencies are updated, and they prevent the compounding community benefit
|
||||
that sharing fixes upstream have on open source projects like the balena CLI. The typical
|
||||
scenario where these patches are used is when the upstream maintainers are unresponsive or
|
||||
unwilling to merge the required fixes, the fixes are very small and specific to the balena CLI,
|
||||
and creating a fork of the upstream repo is likely to be more long-term effort than maintaining
|
||||
the patches.
|
||||
|
||||
## Windows
|
||||
|
||||
Besides the regular npm installation dependencies, the `npm run build:installer` script
|
||||
@ -267,4 +300,3 @@ gotchas to bear in mind:
|
||||
replace: `spec: 'tests/**/*.spec.ts',`
|
||||
|
||||
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`
|
||||
|
||||
|
@ -78,7 +78,7 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
|
||||
The npm installation involves building native (platform-specific) binary modules, which require
|
||||
some development tools to be installed first, as follows.
|
||||
|
||||
> **The balena CLI currently requires Node.js version 10 (min 10.20.0) or 12.**
|
||||
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
|
||||
> **Versions 13 and later are not yet fully supported.**
|
||||
|
||||
### Install development tools
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
import type { JsonVersions } from '../lib/commands/version';
|
||||
|
||||
import { run as oclifRun } from '@oclif/dev-cli';
|
||||
import { run as oclifRun } from 'oclif';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile } from 'child_process';
|
||||
@ -64,7 +64,7 @@ const standaloneZips: PathByPlatform = {
|
||||
|
||||
const oclifInstallers: PathByPlatform = {
|
||||
darwin: dPath('macos', `balena-${version}.pkg`),
|
||||
win32: dPath('win', `balena-${version}-${arch}.exe`),
|
||||
win32: dPath('win32', `balena-${version}-${arch}.exe`),
|
||||
};
|
||||
|
||||
const renamedOclifInstallers: PathByPlatform = {
|
||||
@ -96,6 +96,7 @@ async function diffPkgOutput(pkgOut: string) {
|
||||
'> pkg@',
|
||||
'> Fetching base Node.js binaries',
|
||||
' fetched-',
|
||||
'prebuild-install WARN install No prebuilt binaries found',
|
||||
];
|
||||
const modulesRE =
|
||||
process.platform === 'win32'
|
||||
@ -322,6 +323,10 @@ async function signFilesForNotarization() {
|
||||
if (!item.stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (path.basename(item.path).endsWith('.node.bak')) {
|
||||
console.log('Removing pkg .node.bak file', item.path);
|
||||
fs.unlinkSync(item.path);
|
||||
}
|
||||
if (
|
||||
path.basename(item.path).endsWith('.zip') &&
|
||||
path.dirname(item.path).includes('test')
|
||||
@ -456,7 +461,7 @@ async function notarizeMacInstaller(): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
|
||||
* Run the `oclif pack:win` or `pack:macos` command (depending on the value
|
||||
* of process.platform) to generate the native installers (which end up under
|
||||
* the 'dist' folder). There are some harcoded options such as selecting only
|
||||
* 64-bit binaries under Windows.
|
||||
@ -486,7 +491,7 @@ export async function buildOclifInstaller() {
|
||||
await signFilesForNotarization();
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
|
||||
console.log(`oclif "${packCmd}" "${packOpts.join('" "')}"`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
console.log('=======================================================');
|
||||
await oclifRun([packCmd].concat(...packOpts));
|
||||
|
@ -36,19 +36,12 @@ const capitanoDoc = {
|
||||
{
|
||||
title: 'Fleet',
|
||||
files: [
|
||||
'build/commands/apps.js',
|
||||
'build/commands/fleets.js',
|
||||
'build/commands/app/index.js',
|
||||
'build/commands/fleet/index.js',
|
||||
'build/commands/app/create.js',
|
||||
'build/commands/fleet/create.js',
|
||||
'build/commands/app/purge.js',
|
||||
'build/commands/fleet/purge.js',
|
||||
'build/commands/app/rename.js',
|
||||
'build/commands/fleet/rename.js',
|
||||
'build/commands/app/restart.js',
|
||||
'build/commands/fleet/restart.js',
|
||||
'build/commands/app/rm.js',
|
||||
'build/commands/fleet/rm.js',
|
||||
],
|
||||
},
|
||||
|
@ -6,6 +6,8 @@
|
||||
*
|
||||
* We don't `require('semver')` to allow this script to be run as a npm
|
||||
* 'preinstall' hook, at which point no dependencies have been installed.
|
||||
*
|
||||
* @param {string} version
|
||||
*/
|
||||
function parseSemver(version) {
|
||||
const match = /v?(\d+)\.(\d+).(\d+)/.exec(version);
|
||||
@ -16,6 +18,10 @@ function parseSemver(version) {
|
||||
return [parseInt(major, 10), parseInt(minor, 10), parseInt(patch, 10)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} v1
|
||||
* @param {string} v2
|
||||
*/
|
||||
function semverGte(v1, v2) {
|
||||
let v1Array = parseSemver(v1);
|
||||
let v2Array = parseSemver(v2);
|
||||
|
@ -36,8 +36,8 @@ const run = async (cmd: string) => {
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
p.stdout.pipe(process.stdout);
|
||||
p.stderr.pipe(process.stderr);
|
||||
p.stdout?.pipe(process.stdout);
|
||||
p.stderr?.pipe(process.stderr);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -117,7 +117,7 @@ export async function which(program: string): Promise<string> {
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args?: string[],
|
||||
args: string[] = [],
|
||||
): Promise<void> {
|
||||
const program = await which(programName);
|
||||
let error: Error | undefined;
|
||||
|
@ -8,10 +8,9 @@ _balena() {
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util )
|
||||
main_commands=( build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util )
|
||||
# Sub-completions
|
||||
api_key_cmds=( generate )
|
||||
app_cmds=( create purge rename restart rm )
|
||||
config_cmds=( generate inject read reconfigure write )
|
||||
device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown )
|
||||
devices_cmds=( supported )
|
||||
@ -44,9 +43,6 @@ _balena_sec_cmds() {
|
||||
"api-key")
|
||||
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
|
||||
;;
|
||||
"app")
|
||||
_describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0
|
||||
;;
|
||||
"config")
|
||||
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
||||
;;
|
||||
|
@ -7,10 +7,9 @@ _balena_complete()
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util"
|
||||
main_commands="build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util"
|
||||
# Sub-completions
|
||||
api_key_cmds="generate"
|
||||
app_cmds="create purge rename restart rm"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown"
|
||||
devices_cmds="supported"
|
||||
@ -38,9 +37,6 @@ _balena_complete()
|
||||
api-key)
|
||||
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
|
||||
;;
|
||||
app)
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
config)
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
|
@ -166,19 +166,12 @@ are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
- Fleet
|
||||
|
||||
- [apps](#apps)
|
||||
- [fleets](#fleets)
|
||||
- [app <fleet>](#app-fleet)
|
||||
- [fleet <fleet>](#fleet-fleet)
|
||||
- [app create <name>](#app-create-name)
|
||||
- [fleet create <name>](#fleet-create-name)
|
||||
- [app purge <fleet>](#app-purge-fleet)
|
||||
- [fleet purge <fleet>](#fleet-purge-fleet)
|
||||
- [app rename <fleet> [newname]](#app-rename-fleet-newname)
|
||||
- [fleet rename <fleet> [newname]](#fleet-rename-fleet-newname)
|
||||
- [app restart <fleet>](#app-restart-fleet)
|
||||
- [fleet restart <fleet>](#fleet-restart-fleet)
|
||||
- [app rm <fleet>](#app-rm-fleet)
|
||||
- [fleet rm <fleet>](#fleet-rm-fleet)
|
||||
|
||||
- Authentication
|
||||
@ -327,22 +320,6 @@ the API key name
|
||||
|
||||
# Fleet
|
||||
|
||||
## apps
|
||||
|
||||
Renaming notice: The 'apps' command was renamed to 'fleets', and 'apps'
|
||||
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
|
||||
of the balena CLI (so that a different 'apps' command can be implemented
|
||||
in the future). Use 'fleets' instead of 'apps' to avoid this warning.
|
||||
Find out more at: https://git.io/JRuZr
|
||||
|
||||
For command usage, see 'balena help fleets'
|
||||
|
||||
### Options
|
||||
|
||||
#### -v, --verbose
|
||||
|
||||
No-op since release v12.0.0
|
||||
|
||||
## fleets
|
||||
|
||||
List all your balena fleets.
|
||||
@ -356,28 +333,6 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### -v, --verbose
|
||||
|
||||
No-op since release v12.0.0
|
||||
|
||||
## app <fleet>
|
||||
|
||||
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
|
||||
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
|
||||
of the balena CLI (so that a different 'app' command can be implemented
|
||||
in the future). Use 'fleet' instead of 'app' to avoid this warning.
|
||||
Find out more at: https://git.io/JRuZr
|
||||
|
||||
For command usage, see 'balena help fleet'
|
||||
|
||||
### Arguments
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
## fleet <fleet>
|
||||
|
||||
Display detailed information about a single fleet.
|
||||
@ -407,32 +362,6 @@ fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
## app create <name>
|
||||
|
||||
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
|
||||
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
|
||||
of the balena CLI (so that a different 'app' command can be implemented
|
||||
in the future). Use 'fleet' instead of 'app' to avoid this warning.
|
||||
Find out more at: https://git.io/JRuZr
|
||||
|
||||
For command usage, see 'balena help fleet create'
|
||||
|
||||
### Arguments
|
||||
|
||||
#### NAME
|
||||
|
||||
fleet name
|
||||
|
||||
### Options
|
||||
|
||||
#### -o, --organization ORGANIZATION
|
||||
|
||||
handle of the organization the fleet should belong to
|
||||
|
||||
#### -t, --type TYPE
|
||||
|
||||
fleet device type (Check available types with `balena devices supported`)
|
||||
|
||||
## fleet create <name>
|
||||
|
||||
Create a new balena fleet.
|
||||
@ -474,24 +403,6 @@ handle of the organization the fleet should belong to
|
||||
|
||||
fleet device type (Check available types with `balena devices supported`)
|
||||
|
||||
## app purge <fleet>
|
||||
|
||||
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
|
||||
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
|
||||
of the balena CLI (so that a different 'app' command can be implemented
|
||||
in the future). Use 'fleet' instead of 'app' to avoid this warning.
|
||||
Find out more at: https://git.io/JRuZr
|
||||
|
||||
For command usage, see 'balena help fleet purge'
|
||||
|
||||
### Arguments
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
## fleet purge <fleet>
|
||||
|
||||
Purge data from all devices belonging to a fleet.
|
||||
@ -522,28 +433,6 @@ fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
## app rename <fleet> [newName]
|
||||
|
||||
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
|
||||
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
|
||||
of the balena CLI (so that a different 'app' command can be implemented
|
||||
in the future). Use 'fleet' instead of 'app' to avoid this warning.
|
||||
Find out more at: https://git.io/JRuZr
|
||||
|
||||
For command usage, see 'balena help fleet rename'
|
||||
|
||||
### Arguments
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### NEWNAME
|
||||
|
||||
the new name for the fleet
|
||||
|
||||
### Options
|
||||
|
||||
## fleet rename <fleet> [newName]
|
||||
|
||||
Rename a fleet.
|
||||
@ -581,24 +470,6 @@ the new name for the fleet
|
||||
|
||||
### Options
|
||||
|
||||
## app restart <fleet>
|
||||
|
||||
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
|
||||
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
|
||||
of the balena CLI (so that a different 'app' command can be implemented
|
||||
in the future). Use 'fleet' instead of 'app' to avoid this warning.
|
||||
Find out more at: https://git.io/JRuZr
|
||||
|
||||
For command usage, see 'balena help fleet restart'
|
||||
|
||||
### Arguments
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
## fleet restart <fleet>
|
||||
|
||||
Restart all devices belonging to a fleet.
|
||||
@ -628,28 +499,6 @@ fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
## app rm <fleet>
|
||||
|
||||
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
|
||||
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
|
||||
of the balena CLI (so that a different 'app' command can be implemented
|
||||
in the future). Use 'fleet' instead of 'app' to avoid this warning.
|
||||
Find out more at: https://git.io/JRuZr
|
||||
|
||||
For command usage, see 'balena help fleet rm'
|
||||
|
||||
### Arguments
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
#### -y, --yes
|
||||
|
||||
answer "yes" to all questions (non interactive use)
|
||||
|
||||
## fleet rm <fleet>
|
||||
|
||||
Permanently remove a fleet.
|
||||
@ -795,37 +644,18 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### --app APP
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
#### --v13
|
||||
|
||||
enable selected balena CLI v13 pre-release features, like the renaming
|
||||
from "application" to "fleet" in command output
|
||||
|
||||
## devices supported
|
||||
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
The --verbose option may add extra columns/fields to the output. Currently
|
||||
this includes the "STATE" column which is DEPRECATED and whose values are one
|
||||
of 'new', 'released' or 'discontinued'. However, 'discontinued' device types
|
||||
are only listed if the '--discontinued' option is also used, and this option
|
||||
is also DEPRECATED.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings (for example, the ALIASES column contains a
|
||||
@ -835,23 +665,14 @@ list of zero or more values). The 'jq' utility may be helpful in shell scripts
|
||||
Examples:
|
||||
|
||||
$ balena devices supported
|
||||
$ balena devices supported --verbose
|
||||
$ balena devices supported -vj
|
||||
$ balena devices supported --json
|
||||
|
||||
### Options
|
||||
|
||||
#### --discontinued
|
||||
|
||||
include "discontinued" device types (DEPRECATED)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
#### -v, --verbose
|
||||
|
||||
add extra columns in the tabular output (DEPRECATED)
|
||||
|
||||
## device <uuid>
|
||||
|
||||
Show information about a single device.
|
||||
@ -868,11 +689,6 @@ the device uuid
|
||||
|
||||
### Options
|
||||
|
||||
#### --v13
|
||||
|
||||
enable selected balena CLI v13 pre-release features, like the renaming
|
||||
from "application" to "fleet" in command output
|
||||
|
||||
## device deactivate <uuid>
|
||||
|
||||
Deactivate a device.
|
||||
@ -961,17 +777,9 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### --app APP
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -y, --yes
|
||||
|
||||
@ -1068,17 +876,9 @@ comma-separated list (no blank spaces) of device UUIDs to be moved
|
||||
|
||||
### Options
|
||||
|
||||
#### --app APP
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
## device os-update <uuid>
|
||||
|
||||
@ -1348,7 +1148,7 @@ Examples:
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name or slug
|
||||
fleet name or slug (preferred)
|
||||
|
||||
### Options
|
||||
|
||||
@ -1445,13 +1245,6 @@ environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Renaming notice: The 'app' or 'application' words in table headers
|
||||
or in JSON object keys/properties will be replaced with 'fleet' in
|
||||
the next major version of the CLI (v13). The --v13 option may be used
|
||||
to enable the new names already now, and suppress a warning message.
|
||||
(The --v13 option will be silently ignored in CLI v13.)
|
||||
Find out more at: https://git.io/JRuZr
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena envs --fleet myorg/myfleet
|
||||
@ -1466,17 +1259,9 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### --all
|
||||
|
||||
No-op since balena CLI v12.0.0.
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -c, --config
|
||||
|
||||
@ -1490,19 +1275,10 @@ device UUID
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
#### -v, --verbose
|
||||
|
||||
produce verbose output
|
||||
|
||||
#### -s, --service SERVICE
|
||||
|
||||
service name
|
||||
|
||||
#### --v13
|
||||
|
||||
enable selected balena CLI v13 pre-release features, like the renaming
|
||||
from "application" to "fleet" in command output
|
||||
|
||||
## env rm <id>
|
||||
|
||||
Remove a configuration or environment variable from a fleet, device
|
||||
@ -1631,13 +1407,9 @@ variable value; if omitted, use value from this process' environment
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
@ -1742,17 +1514,9 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### --app APP
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
@ -1794,17 +1558,9 @@ the key string of the tag
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### --app APP
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
@ -1857,17 +1613,9 @@ the optional value associated with the tag
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### --app APP
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
@ -2309,9 +2057,11 @@ Examples:
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2021.10.2.prod
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu-esr
|
||||
|
||||
### Arguments
|
||||
|
||||
@ -2337,8 +2087,8 @@ or 'menu-esr' (interactive menu, ESR versions)
|
||||
|
||||
## os build-config <image> <device-type>
|
||||
|
||||
Interactively generate an OS config once, so that the generated config
|
||||
file can be used in `balena os configure`, skipping the interactive part.
|
||||
Interactively generate a configuration file that can then be used as
|
||||
non-interactive input by the 'balena os configure' command.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -2376,19 +2126,27 @@ following sources, in precedence order:
|
||||
2. A given `config.json` file specified with the `--config` option.
|
||||
3. User input through interactive prompts (text menus).
|
||||
|
||||
The --device-type option may be used to override the fleet's default device
|
||||
type, in case of a fleet with mixed device types.
|
||||
The --device-type option is used to override the fleet's default device type,
|
||||
in case of a fleet with mixed device types.
|
||||
|
||||
The --system-connection (-c) option can be used to inject NetworkManager connection
|
||||
The '--dev' option is used to configure balenaOS to operate in development mode,
|
||||
allowing anauthenticated root ssh access and exposing network ports such as
|
||||
balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true`
|
||||
to be inserted in the 'config.json' file in the image's boot partion. Development
|
||||
mode (as a configurable option) is applicable to balenaOS releases from early
|
||||
2022. Older releases have separate development and production balenaOS images
|
||||
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
|
||||
confuse the balenaOS "development mode" with a device's "local mode", the latter
|
||||
being a supervisor feature that allows the "balena push" command to push a user's
|
||||
application directly to a device in the local network.
|
||||
|
||||
The --system-connection (-c) option is used to inject NetworkManager connection
|
||||
profiles for additional network interfaces, such as cellular/GSM or additional
|
||||
WiFi or ethernet connections. This option may be passed multiple times in case there
|
||||
are multiple files to inject. See connection profile examples and reference at:
|
||||
https://www.balena.io/docs/reference/OS/network/2.x/
|
||||
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
|
||||
|
||||
The --device-api-key option is deprecated and will be removed in a future release.
|
||||
A suitable key is automatically generated or fetched if this option is omitted.
|
||||
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
@ -2409,7 +2167,6 @@ https://docs.microsoft.com/en-us/windows/wsl/about
|
||||
Examples:
|
||||
|
||||
$ balena os configure ../path/rpi3.img --device 7cf02a6
|
||||
$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>
|
||||
$ balena os configure ../path/rpi3.img --fleet myorg/myfleet
|
||||
$ balena os configure ../path/rpi3.img --fleet MyFleet --version 2.12.7
|
||||
$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3
|
||||
@ -2427,17 +2184,9 @@ path to a balenaOS image file, e.g. "rpi3.img"
|
||||
|
||||
ask advanced configuration questions (when in interactive mode)
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### --app APP
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### --config CONFIG
|
||||
|
||||
@ -2459,14 +2208,14 @@ WiFi key (password) (non-interactive configuration)
|
||||
|
||||
WiFi SSID (network name) (non-interactive configuration)
|
||||
|
||||
#### --dev
|
||||
|
||||
Configure balenaOS to operate in development mode
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
device UUID
|
||||
|
||||
#### -k, --device-api-key DEVICE-API-KEY
|
||||
|
||||
custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)
|
||||
|
||||
#### --device-type DEVICE-TYPE
|
||||
|
||||
device type slug (e.g. "raspberrypi3") to override the fleet device type
|
||||
@ -2530,6 +2279,17 @@ Generate a config.json file for a device or fleet.
|
||||
|
||||
The target balenaOS version must be specified with the --version option.
|
||||
|
||||
The '--dev' option is used to configure balenaOS to operate in development mode,
|
||||
allowing anauthenticated root ssh access and exposing network ports such as
|
||||
balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true`
|
||||
to be inserted in the 'config.json' file in the image's boot partion. Development
|
||||
mode (as a configurable option) is applicable to balenaOS releases from early
|
||||
2022. Older releases have separate development and production balenaOS images
|
||||
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
|
||||
confuse the balenaOS "development mode" with a device's "local mode", the latter
|
||||
being a supervisor feature that allows the "balena push" command to push a user's
|
||||
application directly to a device in the local network.
|
||||
|
||||
To configure an image for a fleet of mixed device types, use the --fleet option
|
||||
alongside the --deviceType option to specify the target device type.
|
||||
|
||||
@ -2554,11 +2314,10 @@ Examples:
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
|
||||
$ balena config generate --fleet MyFleet --version 2.12.7
|
||||
$ balena config generate --fleet myorg/myfleet --version 2.12.7
|
||||
$ balena config generate --fleet MyFleet --version 2.12.7 --deviceType fincm3
|
||||
$ balena config generate --fleet MyFleet --version 2.12.7 --output config.json
|
||||
$ balena config generate --fleet MyFleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15
|
||||
$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev
|
||||
$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3
|
||||
$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json
|
||||
$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15
|
||||
|
||||
### Options
|
||||
|
||||
@ -2566,21 +2325,13 @@ Examples:
|
||||
|
||||
a balenaOS version
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### --app APP
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### --appUpdatePollInterval APPUPDATEPOLLINTERVAL
|
||||
|
||||
supervisor cloud polling interval in minutes (e.g. for variable updates)
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### --dev
|
||||
|
||||
Configure balenaOS to operate in development mode
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
@ -2614,6 +2365,10 @@ the wifi ssid to use (used only if --network is set to wifi)
|
||||
|
||||
the wifi key to use (used only if --network is set to wifi)
|
||||
|
||||
#### --appUpdatePollInterval APPUPDATEPOLLINTERVAL
|
||||
|
||||
supervisor cloud polling interval in minutes (e.g. for device variables)
|
||||
|
||||
#### --provisioning-key-name PROVISIONING-KEY-NAME
|
||||
|
||||
custom key name assigned to generated provisioning api key
|
||||
@ -2794,13 +2549,9 @@ the image file path
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --app APP
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -c, --commit COMMIT
|
||||
|
||||
@ -2933,15 +2684,12 @@ compatibility with the standard docker-compose tool, while still allowing a
|
||||
root .dockerignore file (at the overall project root) to filter files and
|
||||
folders that are outside service subdirectories.
|
||||
|
||||
balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||
option if compatibility is required. This option is mutually exclusive with
|
||||
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||
release (v13).
|
||||
balena CLI v11 also took .gitignore files into account. This behavior was
|
||||
deprecated in CLI v12 and removed in CLI v13. Please use .dockerignore files
|
||||
instead.
|
||||
|
||||
Default .dockerignore patterns
|
||||
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
|
||||
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||
A few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||
patterns found in the applicable .dockerignore files, in the following order:
|
||||
```
|
||||
**/.git
|
||||
@ -2954,7 +2702,7 @@ patterns found in the applicable .dockerignore files, in the following order:
|
||||
```
|
||||
These patterns always apply, whether or not .dockerignore files exist in the
|
||||
project. If necessary, the effect of the `**/.git` pattern may be modified by
|
||||
adding counter patterns to the applicable .dockerignore file(s), for example
|
||||
adding exception patterns to the applicable .dockerignore file(s), for example
|
||||
`!mysubmodule/.git`. For documentation on pattern format, see:
|
||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
- https://www.npmjs.com/package/@balena/dockerignore
|
||||
@ -3062,10 +2810,6 @@ separated by a colon, e.g:
|
||||
Note that if the service name cannot be found in the composition, the entire
|
||||
left hand side of the = character will be treated as the variable name.
|
||||
|
||||
#### -l, --convert-eol
|
||||
|
||||
No-op and deprecated since balena CLI v12.0.0
|
||||
|
||||
#### --noconvert-eol
|
||||
|
||||
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||
@ -3074,16 +2818,6 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||
|
||||
Have each service use its own .dockerignore file. See "balena help push".
|
||||
|
||||
#### -g, --gitignore
|
||||
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is
|
||||
required until your project can be adapted.
|
||||
|
||||
#### -G, --nogitignore
|
||||
|
||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
|
||||
|
||||
#### --release-tag RELEASE-TAG
|
||||
|
||||
Set release tags if the image build is successful (balenaCloud only). Multiple
|
||||
@ -3226,15 +2960,12 @@ compatibility with the standard docker-compose tool, while still allowing a
|
||||
root .dockerignore file (at the overall project root) to filter files and
|
||||
folders that are outside service subdirectories.
|
||||
|
||||
balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||
option if compatibility is required. This option is mutually exclusive with
|
||||
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||
release (v13).
|
||||
balena CLI v11 also took .gitignore files into account. This behavior was
|
||||
deprecated in CLI v12 and removed in CLI v13. Please use .dockerignore files
|
||||
instead.
|
||||
|
||||
Default .dockerignore patterns
|
||||
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
|
||||
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||
A few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||
patterns found in the applicable .dockerignore files, in the following order:
|
||||
```
|
||||
**/.git
|
||||
@ -3247,7 +2978,7 @@ patterns found in the applicable .dockerignore files, in the following order:
|
||||
```
|
||||
These patterns always apply, whether or not .dockerignore files exist in the
|
||||
project. If necessary, the effect of the `**/.git` pattern may be modified by
|
||||
adding counter patterns to the applicable .dockerignore file(s), for example
|
||||
adding exception patterns to the applicable .dockerignore file(s), for example
|
||||
`!mysubmodule/.git`. For documentation on pattern format, see:
|
||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
- https://www.npmjs.com/package/@balena/dockerignore
|
||||
@ -3277,13 +3008,9 @@ the architecture to build for
|
||||
|
||||
the type of device this build is for
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -e, --emulated
|
||||
|
||||
@ -3293,6 +3020,10 @@ Use QEMU for ARM architecture emulation during the image build
|
||||
|
||||
Alternative Dockerfile name/path, relative to the source folder
|
||||
|
||||
#### --dockercompose DOCKERCOMPOSE
|
||||
|
||||
Alternative docker-compose.yml name in the source root folder
|
||||
|
||||
#### --logs
|
||||
|
||||
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
|
||||
@ -3301,16 +3032,6 @@ No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by defau
|
||||
|
||||
Hide the image build log output (produce less verbose output)
|
||||
|
||||
#### -g, --gitignore
|
||||
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||
until your project can be adapted.
|
||||
|
||||
#### -G, --nogitignore
|
||||
|
||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
|
||||
|
||||
#### -m, --multi-dockerignore
|
||||
|
||||
Have each service use its own .dockerignore file. See "balena help build".
|
||||
@ -3323,10 +3044,6 @@ Disable project validation check of 'docker-compose.yml' file in parent folder
|
||||
|
||||
Path to a YAML or JSON file with passwords for a private Docker registry
|
||||
|
||||
#### -l, --convert-eol
|
||||
|
||||
No-op and deprecated since balena CLI v12.0.0
|
||||
|
||||
#### --noconvert-eol
|
||||
|
||||
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||
@ -3456,15 +3173,12 @@ compatibility with the standard docker-compose tool, while still allowing a
|
||||
root .dockerignore file (at the overall project root) to filter files and
|
||||
folders that are outside service subdirectories.
|
||||
|
||||
balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||
option if compatibility is required. This option is mutually exclusive with
|
||||
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||
release (v13).
|
||||
balena CLI v11 also took .gitignore files into account. This behavior was
|
||||
deprecated in CLI v12 and removed in CLI v13. Please use .dockerignore files
|
||||
instead.
|
||||
|
||||
Default .dockerignore patterns
|
||||
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
|
||||
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||
A few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||
patterns found in the applicable .dockerignore files, in the following order:
|
||||
```
|
||||
**/.git
|
||||
@ -3477,7 +3191,7 @@ patterns found in the applicable .dockerignore files, in the following order:
|
||||
```
|
||||
These patterns always apply, whether or not .dockerignore files exist in the
|
||||
project. If necessary, the effect of the `**/.git` pattern may be modified by
|
||||
adding counter patterns to the applicable .dockerignore file(s), for example
|
||||
adding exception patterns to the applicable .dockerignore file(s), for example
|
||||
`!mysubmodule/.git`. For documentation on pattern format, see:
|
||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
- https://www.npmjs.com/package/@balena/dockerignore
|
||||
@ -3534,6 +3248,10 @@ Use QEMU for ARM architecture emulation during the image build
|
||||
|
||||
Alternative Dockerfile name/path, relative to the source folder
|
||||
|
||||
#### --dockercompose DOCKERCOMPOSE
|
||||
|
||||
Alternative docker-compose.yml name in the source root folder
|
||||
|
||||
#### --logs
|
||||
|
||||
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
|
||||
@ -3542,16 +3260,6 @@ No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by defau
|
||||
|
||||
Hide the image build log output (produce less verbose output)
|
||||
|
||||
#### -g, --gitignore
|
||||
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||
until your project can be adapted.
|
||||
|
||||
#### -G, --nogitignore
|
||||
|
||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
|
||||
|
||||
#### -m, --multi-dockerignore
|
||||
|
||||
Have each service use its own .dockerignore file. See "balena help build".
|
||||
@ -3564,10 +3272,6 @@ Disable project validation check of 'docker-compose.yml' file in parent folder
|
||||
|
||||
Path to a YAML or JSON file with passwords for a private Docker registry
|
||||
|
||||
#### -l, --convert-eol
|
||||
|
||||
No-op and deprecated since balena CLI v12.0.0
|
||||
|
||||
#### --noconvert-eol
|
||||
|
||||
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||
@ -3674,13 +3378,9 @@ the IP or hostname of device
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name, slug (preferred), or numeric ID
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -i, --pollInterval POLLINTERVAL
|
||||
|
||||
@ -3766,13 +3466,9 @@ enable|disable support access
|
||||
|
||||
comma-separated list (no spaces) of device UUIDs
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
DEPRECATED alias for -f, --fleet
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
comma-separated list (no spaces) of fleet names or org/name slugs
|
||||
comma-separated list (no spaces) of fleet names or slugs (preferred)
|
||||
|
||||
#### -t, --duration DURATION
|
||||
|
||||
|
15
gulpfile.js
15
gulpfile.js
@ -1,15 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const inlinesource = require('gulp-inline-source');
|
||||
|
||||
const OPTIONS = {
|
||||
files: {
|
||||
pages: 'lib/auth/pages/*.ejs',
|
||||
},
|
||||
};
|
||||
|
||||
gulp.task('pages', () =>
|
||||
gulp
|
||||
.src(OPTIONS.files.pages)
|
||||
.pipe(inlinesource())
|
||||
.pipe(gulp.dest('build/auth/pages')),
|
||||
);
|
@ -14,57 +14,44 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as url from 'url';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
/**
|
||||
* @summary Get dashboard CLI login URL
|
||||
* @function
|
||||
* @protected
|
||||
* Get dashboard CLI login URL
|
||||
*
|
||||
* @param {String} callbackUrl - callback url
|
||||
* @fulfil {String} - dashboard login url
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* utils.getDashboardLoginURL('http://127.0.0.1:3000').then (url) ->
|
||||
* console.log(url)
|
||||
* @param callbackUrl - Callback url, e.g. 'http://127.0.0.1:3000'
|
||||
* @returns Dashboard login URL, e.g.:
|
||||
* 'https://dashboard.balena-cloud.com/login/cli/http%253A%252F%252F127.0.0.1%253A59581%252Fauth'
|
||||
*/
|
||||
export const getDashboardLoginURL = (callbackUrl: string) => {
|
||||
export async function getDashboardLoginURL(
|
||||
callbackUrl: string,
|
||||
): Promise<string> {
|
||||
// Encode percentages signs from the escaped url
|
||||
// characters to avoid angular getting confused.
|
||||
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25');
|
||||
|
||||
return getBalenaSdk()
|
||||
.settings.get('dashboardUrl')
|
||||
.then((dashboardUrl) =>
|
||||
url.resolve(dashboardUrl, `/login/cli/${callbackUrl}`),
|
||||
);
|
||||
};
|
||||
const [{ URL }, dashboardUrl] = await Promise.all([
|
||||
import('url'),
|
||||
getBalenaSdk().settings.get('dashboardUrl'),
|
||||
]);
|
||||
return new URL(`/login/cli/${callbackUrl}`, dashboardUrl).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Log in using a token, but only if the token is valid
|
||||
* @function
|
||||
* @protected
|
||||
* Log in using a token, but only if the token is valid.
|
||||
*
|
||||
* @description
|
||||
* This function checks that the token is not only well-structured
|
||||
* but that it also authenticates with the server successfully.
|
||||
*
|
||||
* If authenticated, the token is persisted, if not then the previous
|
||||
* login state is restored.
|
||||
*
|
||||
* @param {String} token - session token or api key
|
||||
* @fulfil {Boolean} - whether the login was successful or not
|
||||
* @returns {Promise}
|
||||
*
|
||||
* utils.loginIfTokenValid('...').then (loggedIn) ->
|
||||
* if loggedIn
|
||||
* console.log('Token is valid!')
|
||||
* @param token - session token or api key
|
||||
* @returns whether the login was successful or not
|
||||
*/
|
||||
export const loginIfTokenValid = async (token: string): Promise<boolean> => {
|
||||
if (_.isEmpty(token?.trim())) {
|
||||
export async function loginIfTokenValid(token?: string): Promise<boolean> {
|
||||
token = (token || '').trim();
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
const balena = getBalenaSdk();
|
||||
@ -86,4 +73,4 @@ export const loginIfTokenValid = async (token: string): Promise<boolean> => {
|
||||
}
|
||||
}
|
||||
return isLoggedIn;
|
||||
};
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
NotAvailableInOfflineModeError,
|
||||
} from './errors';
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import * as output from './framework/output';
|
||||
|
||||
export default abstract class BalenaCommand extends Command {
|
||||
/**
|
||||
@ -167,4 +168,7 @@ export default abstract class BalenaCommand extends Command {
|
||||
await this.getStdin();
|
||||
}
|
||||
}
|
||||
|
||||
protected outputMessage = output.outputMessage;
|
||||
protected outputData = output.outputData;
|
||||
}
|
||||
|
@ -1,181 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 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 { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
import type { Application } from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { appToFleetCmdMsg, warnify } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class FleetCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create a fleet.
|
||||
|
||||
Create a new balena fleet.
|
||||
|
||||
You can specify the organization the fleet should belong to using
|
||||
the \`--organization\` option. The organization's handle, not its name,
|
||||
should be provided. Organization handles can be listed with the
|
||||
\`balena orgs\` command.
|
||||
|
||||
The fleet's default device type is specified with the \`--type\` option.
|
||||
The \`balena devices supported\` command can be used to list the available
|
||||
device types.
|
||||
|
||||
Interactive dropdowns will be shown for selection if no device type or
|
||||
organization is specified and there are multiple options to choose from.
|
||||
If there is a single option to choose from, it will be chosen automatically.
|
||||
This interactive behavior can be disabled by explicitly specifying a device
|
||||
type and organization.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet create MyFleet',
|
||||
'$ balena fleet create MyFleet --organization mmyorg',
|
||||
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'fleet name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'fleet create <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
organization: flags.string({
|
||||
char: 'o',
|
||||
description: 'handle of the organization the fleet should belong to',
|
||||
}),
|
||||
type: flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'fleet device type (Check available types with `balena devices supported`)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params, flags: options } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCreateCmd);
|
||||
|
||||
// Ascertain device type
|
||||
const deviceType =
|
||||
options.type ||
|
||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
||||
|
||||
// Ascertain organization
|
||||
const organization =
|
||||
options.organization?.toLowerCase() || (await this.getOrganization());
|
||||
|
||||
// Create application
|
||||
let application: Application;
|
||||
try {
|
||||
application = await getBalenaSdk().models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
organization,
|
||||
});
|
||||
} catch (err) {
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
throw new ExpectedError(
|
||||
`Error: fleet "${params.name}" already exists in organization "${organization}".`,
|
||||
);
|
||||
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
|
||||
// BalenaRequestError: Request error: Unauthorized
|
||||
throw new ExpectedError(
|
||||
`Error: You are not authorized to create fleets in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Output
|
||||
const { isV13 } = await import('../../utils/version');
|
||||
console.log(
|
||||
isV13()
|
||||
? `Fleet created: slug "${application.slug}", device type "${deviceType}"`
|
||||
: `Fleet created: ${application.slug} (${deviceType}, id ${application.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
async getOrganization() {
|
||||
const { getOwnOrganizations } = await import('../../utils/sdk');
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk());
|
||||
|
||||
if (organizations.length === 0) {
|
||||
// User is not a member of any organizations (should not happen).
|
||||
throw new Error('This account is not a member of any organizations');
|
||||
} else if (organizations.length === 1) {
|
||||
// User is a member of only one organization - use this.
|
||||
return organizations[0].handle;
|
||||
} else {
|
||||
// User is a member of multiple organizations -
|
||||
const { selectOrganization } = await import('../../utils/patterns');
|
||||
return selectOrganization(organizations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppCreateCmd extends FleetCreateCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet create' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet create'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app create <name>';
|
||||
public static args = FleetCreateCmd.args;
|
||||
public static flags = FleetCreateCmd.flags;
|
||||
public static authenticated = FleetCreateCmd.authenticated;
|
||||
public static primary = FleetCreateCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCreateCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 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 type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
import type { Release } from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export class FleetCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display information about a single fleet.
|
||||
|
||||
Display detailed information about a single fleet.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena fleet MyFleet',
|
||||
'$ balena fleet myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const application = (await getApplication(getBalenaSdk(), params.fleet, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
},
|
||||
})) as ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
// For display purposes:
|
||||
device_type: string;
|
||||
commit?: string;
|
||||
};
|
||||
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
application.commit = application.should_be_running__release[0]?.commit;
|
||||
|
||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
||||
console.log(`== ${application.app_name}`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(application, [
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppCmd extends FleetCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app <fleet>';
|
||||
public static args = FleetCmd.args;
|
||||
public static flags = FleetCmd.flags;
|
||||
public static authenticated = FleetCmd.authenticated;
|
||||
public static primary = FleetCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-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 type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export class FleetPurgeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Purge data from a fleet.
|
||||
|
||||
Purge data from all devices belonging to a fleet.
|
||||
This will clear the fleet's '/data' directory.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet purge MyFleet',
|
||||
'$ balena fleet purge myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet purge <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// balena.models.application.purge only accepts a numeric id
|
||||
// so we must first fetch the app to get it's id,
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
try {
|
||||
await balena.models.application.purge(application.id);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('no online device(s) found')) {
|
||||
// application.purge throws an error if no devices are online
|
||||
// ignore in this case.
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppPurgeCmd extends FleetPurgeCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet purge' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet purge'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app purge <fleet>';
|
||||
public static args = FleetPurgeCmd.args;
|
||||
public static flags = FleetPurgeCmd.flags;
|
||||
public static authenticated = FleetPurgeCmd.authenticated;
|
||||
public static primary = FleetPurgeCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-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 type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
import type { ApplicationType } from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
newName?: string;
|
||||
}
|
||||
|
||||
export class FleetRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Rename a fleet.
|
||||
|
||||
Rename a fleet.
|
||||
|
||||
Note, if the \`newName\` parameter is omitted, it will be
|
||||
prompted for interactively.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet rename OldName',
|
||||
'$ balena fleet rename OldName NewName',
|
||||
'$ balena fleet rename myorg/oldname NewName',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
ca.fleetRequired,
|
||||
{
|
||||
name: 'newName',
|
||||
description: 'the new name for the fleet',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'fleet rename <fleet> [newName]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
|
||||
|
||||
const { validateApplicationName } = await import('../../utils/validation');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: ['is_legacy'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check app exists
|
||||
if (!application) {
|
||||
throw new ExpectedError(`Error: fleet ${params.fleet} not found.`);
|
||||
}
|
||||
|
||||
// Check app supports renaming
|
||||
const appType = (application.application_type as ApplicationType[])?.[0];
|
||||
if (appType.is_legacy) {
|
||||
throw new ExpectedError(
|
||||
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Ascertain new name
|
||||
const newName =
|
||||
params.newName ||
|
||||
(await getCliForm().ask({
|
||||
message: 'Please enter the new name for this fleet:',
|
||||
type: 'input',
|
||||
validate: validateApplicationName,
|
||||
})) ||
|
||||
'';
|
||||
|
||||
// Rename
|
||||
try {
|
||||
await balena.models.application.rename(application.id, newName);
|
||||
} catch (e) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
if ((e.message || '').toLowerCase().includes('unique')) {
|
||||
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Get application again, to be sure of results
|
||||
const renamedApplication = await balena.models.application.get(
|
||||
application.id,
|
||||
);
|
||||
|
||||
// Output result
|
||||
console.log(`Fleet renamed`);
|
||||
console.log('From:');
|
||||
console.log(`\tname: ${application.app_name}`);
|
||||
console.log(`\tslug: ${application.slug}`);
|
||||
console.log('To:');
|
||||
console.log(`\tname: ${renamedApplication.app_name}`);
|
||||
console.log(`\tslug: ${renamedApplication.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppRenameCmd extends FleetRenameCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet rename' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet rename'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app rename <fleet> [newName]';
|
||||
public static args = FleetRenameCmd.args;
|
||||
public static flags = FleetRenameCmd.flags;
|
||||
public static authenticated = FleetRenameCmd.authenticated;
|
||||
public static primary = FleetRenameCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-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 type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export class FleetRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart a fleet.
|
||||
|
||||
Restart all devices belonging to a fleet.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet restart MyFleet',
|
||||
'$ balena fleet restart myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet restart <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRestartCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
await balena.models.application.restart(application.id);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppRestartCmd extends FleetRestartCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet restart' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet restart'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app restart <fleet>';
|
||||
public static args = FleetRestartCmd.args;
|
||||
public static flags = FleetRestartCmd.flags;
|
||||
public static authenticated = FleetRestartCmd.authenticated;
|
||||
public static primary = FleetRestartCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-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 type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export class FleetRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a fleet.
|
||||
|
||||
Permanently remove a fleet.
|
||||
|
||||
The --yes option may be used to avoid interactive confirmation.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet rm MyFleet',
|
||||
'$ balena fleet rm MyFleet --yes',
|
||||
'$ balena fleet rm myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet rm <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params, flags: options } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRmCmd);
|
||||
|
||||
const { confirm } = await import('../../utils/patterns');
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Confirm
|
||||
await confirm(
|
||||
options.yes ?? false,
|
||||
`Are you sure you want to delete fleet ${params.fleet}?`,
|
||||
);
|
||||
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
// Remove
|
||||
await balena.models.application.remove(application.id);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppRmCmd extends FleetRmCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet rm' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet rm'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app rm <fleet>';
|
||||
public static args = FleetRmCmd.args;
|
||||
public static flags = FleetRmCmd.flags;
|
||||
public static authenticated = FleetRmCmd.authenticated;
|
||||
public static primary = FleetRmCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRmCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 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 { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { appToFleetCmdMsg, warnify } from '../utils/messages';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
device_count?: number;
|
||||
online_devices?: number;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export class FleetsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all fleets.
|
||||
|
||||
List all your balena fleets.
|
||||
|
||||
For detailed information on a particular fleet, use
|
||||
\`balena fleet <fleet>\`
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena fleets'];
|
||||
|
||||
public static usage = 'fleets';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
verbose: flags.boolean({
|
||||
default: false,
|
||||
char: 'v',
|
||||
description: 'No-op since release v12.0.0',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
protected useAppWord = false;
|
||||
|
||||
public async run(_parserOutput?: ParserOutput<FlagsDef, {}>) {
|
||||
_parserOutput ||= this.parse<FlagsDef, {}>(FleetsCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Get applications
|
||||
const applications = (await balena.models.application.getAll({
|
||||
$select: ['id', 'app_name', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
owns__device: { $select: 'is_online' },
|
||||
},
|
||||
})) as ExtendedApplication[];
|
||||
|
||||
const _ = await import('lodash');
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
application.device_count = application.owns__device?.length ?? 0;
|
||||
application.online_devices = _.sumBy(application.owns__device, (d) =>
|
||||
d.is_online === true ? 1 : 0,
|
||||
);
|
||||
// @ts-expect-error
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
});
|
||||
|
||||
// Display
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
this.useAppWord ? 'app_name' : 'app_name => NAME',
|
||||
'slug',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const appsToFleetsRenameMsg = appToFleetCmdMsg
|
||||
.replace(/'app'/g, "'apps'")
|
||||
.replace(/'fleet'/g, "'fleets'");
|
||||
|
||||
export default class AppsCmd extends FleetsCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleets' command
|
||||
|
||||
${appsToFleetsRenameMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleets'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'apps';
|
||||
public static args = FleetsCmd.args;
|
||||
public static flags = FleetsCmd.flags;
|
||||
public static authenticated = FleetsCmd.authenticated;
|
||||
public static primary = FleetsCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, {}>(AppsCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appsToFleetsRenameMsg));
|
||||
}
|
||||
this.useAppWord = true;
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
@ -22,22 +22,18 @@ import * as cf from '../utils/common-flags';
|
||||
import * as compose from '../utils/compose';
|
||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||
import {
|
||||
appToFleetFlagMsg,
|
||||
buildArgDeprecation,
|
||||
dockerignoreHelp,
|
||||
registrySecretsHelp,
|
||||
warnify,
|
||||
} from '../utils/messages';
|
||||
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
||||
import { buildProject, composeCliFlags } from '../utils/compose_ts';
|
||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
import { isV13 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
arch?: string;
|
||||
deviceType?: string;
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
source?: string; // Not part of command profile - source param copied here.
|
||||
help: void;
|
||||
@ -96,7 +92,6 @@ ${dockerignoreHelp}
|
||||
description: 'the type of device this build is for',
|
||||
char: 'd',
|
||||
}),
|
||||
...(isV13() ? {} : { application: cf.application }),
|
||||
fleet: cf.fleet,
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
@ -112,12 +107,7 @@ ${dockerignoreHelp}
|
||||
BuildCmd,
|
||||
);
|
||||
|
||||
if (options.application && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.fleet;
|
||||
|
||||
await Command.checkLoggedInIf(!!options.application);
|
||||
await Command.checkLoggedInIf(!!options.fleet);
|
||||
|
||||
(await import('events')).defaultMaxListeners = 1000;
|
||||
|
||||
@ -161,10 +151,8 @@ ${dockerignoreHelp}
|
||||
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
||||
// Validate option combinations
|
||||
if (
|
||||
(opts.application == null &&
|
||||
(opts.arch == null || opts.deviceType == null)) ||
|
||||
(opts.application != null &&
|
||||
(opts.arch != null || opts.deviceType != null))
|
||||
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
||||
(opts.fleet != null && (opts.arch != null || opts.deviceType != null))
|
||||
) {
|
||||
const { ExpectedError } = await import('../errors');
|
||||
throw new ExpectedError(
|
||||
@ -189,9 +177,9 @@ ${dockerignoreHelp}
|
||||
}
|
||||
|
||||
protected async getAppAndResolveArch(opts: FlagsDef) {
|
||||
if (opts.application) {
|
||||
if (opts.fleet) {
|
||||
const { getAppWithArch } = await import('../utils/helpers');
|
||||
const app = await getAppWithArch(opts.application);
|
||||
const app = await getAppWithArch(opts.fleet);
|
||||
opts.arch = app.arch;
|
||||
opts.deviceType = app.is_for__device_type[0].slug;
|
||||
return app;
|
||||
@ -271,7 +259,6 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore, // v13: delete this line
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
}
|
||||
|
@ -19,19 +19,13 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
|
||||
import type { PineDeferred } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
version: string; // OS version
|
||||
application?: string;
|
||||
app?: string; // application alias
|
||||
fleet?: string;
|
||||
dev?: boolean; // balenaOS development variant
|
||||
device?: string;
|
||||
deviceApiKey?: string;
|
||||
deviceType?: string;
|
||||
@ -54,6 +48,8 @@ export default class ConfigGenerateCmd extends Command {
|
||||
|
||||
The target balenaOS version must be specified with the --version option.
|
||||
|
||||
${devModeInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
To configure an image for a fleet of mixed device types, use the --fleet option
|
||||
alongside the --deviceType option to specify the target device type.
|
||||
|
||||
@ -68,11 +64,10 @@ export default class ConfigGenerateCmd extends Command {
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet MyFleet --version 2.12.7',
|
||||
'$ balena config generate --fleet myorg/myfleet --version 2.12.7',
|
||||
'$ balena config generate --fleet MyFleet --version 2.12.7 --deviceType fincm3',
|
||||
'$ balena config generate --fleet MyFleet --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet MyFleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
||||
];
|
||||
|
||||
public static usage = 'config generate';
|
||||
@ -82,22 +77,11 @@ export default class ConfigGenerateCmd extends Command {
|
||||
description: 'a balenaOS version',
|
||||
required: true,
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device'],
|
||||
},
|
||||
app: { ...cf.app, exclusive: ['application', 'fleet', 'device'] },
|
||||
appUpdatePollInterval: flags.string({
|
||||
description: 'DEPRECATED alias for --updatePollInterval',
|
||||
}),
|
||||
}),
|
||||
fleet: { ...cf.fleet, exclusive: ['application', 'app', 'device'] },
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
dev: cf.dev,
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['application', 'app', 'fleet', 'provisioning-key-name'],
|
||||
exclusive: ['fleet', 'provisioning-key-name'],
|
||||
},
|
||||
deviceApiKey: flags.string({
|
||||
description:
|
||||
@ -130,7 +114,7 @@ export default class ConfigGenerateCmd extends Command {
|
||||
}),
|
||||
appUpdatePollInterval: flags.string({
|
||||
description:
|
||||
'supervisor cloud polling interval in minutes (e.g. for variable updates)',
|
||||
'supervisor cloud polling interval in minutes (e.g. for device variables)',
|
||||
}),
|
||||
'provisioning-key-name': flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
@ -173,7 +157,7 @@ export default class ConfigGenerateCmd extends Command {
|
||||
resourceDeviceType = device.is_of__device_type[0].slug;
|
||||
} else {
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
application = (await getApplication(balena, options.application!, {
|
||||
application = (await getApplication(balena, options.fleet!, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
@ -188,7 +172,7 @@ export default class ConfigGenerateCmd extends Command {
|
||||
);
|
||||
|
||||
// Check compatibility if application and deviceType provided
|
||||
if (options.application && options.deviceType) {
|
||||
if (options.fleet && options.deviceType) {
|
||||
const appDeviceManifest = await balena.models.device.getManifestBySlug(
|
||||
resourceDeviceType,
|
||||
);
|
||||
@ -198,7 +182,7 @@ export default class ConfigGenerateCmd extends Command {
|
||||
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
|
||||
) {
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
`Device type ${options.deviceType} is incompatible with fleet ${options.application}`,
|
||||
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -207,9 +191,10 @@ export default class ConfigGenerateCmd extends Command {
|
||||
// Pass params as an override: if there is any param with exactly the same name as a
|
||||
// required option, that value is used (and the corresponding question is not asked)
|
||||
const answers = await getCliForm().run(deviceManifest.options, {
|
||||
override: options,
|
||||
override: { ...options, app: options.fleet, application: options.fleet },
|
||||
});
|
||||
answers.version = options.version;
|
||||
answers.developmentMode = options.dev;
|
||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||
|
||||
// Generate config
|
||||
@ -253,19 +238,14 @@ export default class ConfigGenerateCmd extends Command {
|
||||
protected async validateOptions(options: FlagsDef) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
// Prefer options.application over options.app
|
||||
delete options.app;
|
||||
|
||||
if (options.device == null && options.application == null) {
|
||||
if (options.device == null && options.fleet == null) {
|
||||
throw new ExpectedError(this.missingDeviceOrAppMessage);
|
||||
}
|
||||
|
||||
if (!options.application && options.deviceType) {
|
||||
if (!options.fleet && options.deviceType) {
|
||||
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
||||
}
|
||||
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
||||
await validateDevOptionAndWarn(options.dev, options.version);
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export default class ConfigInjectCmd extends Command {
|
||||
public static usage = 'config inject <file>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: cf.deviceTypeIgnored,
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
@ -47,7 +47,7 @@ export default class ConfigReadCmd extends Command {
|
||||
public static usage = 'config read';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: cf.deviceTypeIgnored,
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
|
@ -50,7 +50,7 @@ export default class ConfigReconfigureCmd extends Command {
|
||||
public static usage = 'config reconfigure';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: cf.deviceTypeIgnored,
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
advanced: flags.boolean({
|
||||
description: 'show advanced commands',
|
||||
|
@ -64,7 +64,7 @@ export default class ConfigWriteCmd extends Command {
|
||||
public static usage = 'config write <key> <value>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: cf.deviceTypeIgnored,
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
@ -114,7 +114,7 @@ ${dockerignoreHelp}
|
||||
];
|
||||
|
||||
public static usage = 'deploy <fleet> [image]';
|
||||
|
||||
// TODO: docker-compose naming
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
source: flags.string({
|
||||
description:
|
||||
@ -319,7 +319,6 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore, // v13: delete this line
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
||||
|
@ -21,15 +21,13 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { appToFleetOutputMsg, warnify } from '../../utils/messages';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
import type { Application, Release } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
application_name?: string;
|
||||
fleet: string; // 'org/name' slug
|
||||
device_type?: string;
|
||||
commit?: string;
|
||||
last_seen?: string;
|
||||
@ -46,7 +44,6 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
v13: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -74,17 +71,13 @@ export default class DeviceCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
v13: cf.v13,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceCmd,
|
||||
);
|
||||
const useAppWord = !options.v13 && !isV13();
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
@ -121,8 +114,8 @@ export default class DeviceCmd extends Command {
|
||||
|
||||
const belongsToApplication =
|
||||
device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
device.fleet = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].slug
|
||||
: 'N/a';
|
||||
|
||||
device.device_type = device.is_of__device_type[0].slug;
|
||||
@ -170,10 +163,6 @@ export default class DeviceCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
if (useAppWord && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetOutputMsg));
|
||||
}
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
@ -184,7 +173,7 @@ export default class DeviceCmd extends Command {
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
useAppWord ? 'application_name' : 'application_name => FLEET',
|
||||
'fleet',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
|
@ -19,17 +19,10 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import { runCommand } from '../../utils/helpers';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
yes: boolean;
|
||||
advanced: boolean;
|
||||
@ -82,12 +75,6 @@ export default class DeviceInitCmd extends Command {
|
||||
public static usage = 'device init';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
}),
|
||||
fleet: cf.fleet,
|
||||
yes: cf.yes,
|
||||
advanced: flags.boolean({
|
||||
@ -130,17 +117,10 @@ export default class DeviceInitCmd extends Command {
|
||||
const logger = await Command.getLogger();
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
// Consolidate application options
|
||||
options.application ||= options.app || options.fleet;
|
||||
delete options.app;
|
||||
|
||||
// Get application and
|
||||
const application = (await getApplication(
|
||||
balena,
|
||||
options['application'] ||
|
||||
options.fleet ||
|
||||
(
|
||||
await (await import('../../utils/patterns')).selectApplication()
|
||||
).id,
|
||||
@ -155,7 +135,7 @@ export default class DeviceInitCmd extends Command {
|
||||
|
||||
// Register new device
|
||||
const deviceUuid = balena.models.device.generateUniqueKey();
|
||||
console.info(`Registering to ${application.app_name}: ${deviceUuid}`);
|
||||
console.info(`Registering to ${application.slug}: ${deviceUuid}`);
|
||||
await balena.models.device.register(application.id, deviceUuid);
|
||||
const device = await balena.models.device.get(deviceUuid);
|
||||
|
||||
|
@ -27,12 +27,7 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
type ExtendedDevice = PineTypedResult<
|
||||
Device,
|
||||
@ -42,8 +37,6 @@ type ExtendedDevice = PineTypedResult<
|
||||
};
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
help: void;
|
||||
}
|
||||
@ -82,7 +75,6 @@ export default class DeviceMoveCmd extends Command {
|
||||
public static usage = 'device move <uuid(s)>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13() ? {} : { app: cf.app, application: cf.application }),
|
||||
fleet: cf.fleet,
|
||||
help: cf.help,
|
||||
};
|
||||
@ -94,11 +86,6 @@ export default class DeviceMoveCmd extends Command {
|
||||
DeviceMoveCmd,
|
||||
);
|
||||
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
@ -132,8 +119,8 @@ export default class DeviceMoveCmd extends Command {
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
// Get destination application
|
||||
const application = options.application
|
||||
? await getApplication(balena, options.application)
|
||||
const application = options.fleet
|
||||
? await getApplication(balena, options.fleet)
|
||||
: await this.interactivelySelectApplication(balena, devices);
|
||||
|
||||
// Move each device
|
||||
|
@ -75,7 +75,7 @@ export default class DeviceRegisterCmd extends Command {
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
|
||||
|
||||
console.info(`Registering to ${application.app_name}: ${uuid}`);
|
||||
console.info(`Registering to ${application.slug}: ${uuid}`);
|
||||
|
||||
const result = await balena.models.device.register(application.id, uuid);
|
||||
|
||||
|
@ -20,30 +20,20 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
appToFleetOutputMsg,
|
||||
jsonInfo,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||
|
||||
import type { Application } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
application_name?: string | null;
|
||||
fleet?: string | null; // 'org/name' slug
|
||||
device_type?: string | null;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
v13: boolean;
|
||||
}
|
||||
|
||||
export default class DevicesCmd extends Command {
|
||||
@ -67,50 +57,25 @@ export default class DevicesCmd extends Command {
|
||||
public static usage = 'devices';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'v13'],
|
||||
},
|
||||
app: { ...cf.app, exclusive: ['application', 'fleet', 'v13'] },
|
||||
}),
|
||||
fleet: { ...cf.fleet, exclusive: ['app', 'application'] },
|
||||
fleet: cf.fleet,
|
||||
json: cf.json,
|
||||
help: cf.help,
|
||||
v13: cf.v13,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
protected useAppWord = false;
|
||||
protected hasWarned = false;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
||||
this.useAppWord = !options.fleet && !options.v13 && !isV13();
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if (
|
||||
(options.application || options.app) &&
|
||||
!options.json &&
|
||||
process.stderr.isTTY
|
||||
) {
|
||||
this.hasWarned = true;
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
// Consolidate application options
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
let devices;
|
||||
|
||||
if (options.application != null) {
|
||||
if (options.fleet != null) {
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const application = await getApplication(balena, options.application);
|
||||
const application = await getApplication(balena, options.fleet);
|
||||
devices = (await balena.models.device.getAllByApplication(
|
||||
application.id,
|
||||
expandForAppName,
|
||||
@ -126,7 +91,7 @@ export default class DevicesCmd extends Command {
|
||||
|
||||
const belongsToApplication =
|
||||
device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]?.app_name || null;
|
||||
device.fleet = belongsToApplication?.[0]?.slug || null;
|
||||
|
||||
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
|
||||
|
||||
@ -134,16 +99,12 @@ export default class DevicesCmd extends Command {
|
||||
return device;
|
||||
});
|
||||
|
||||
const jName = this.useAppWord ? 'application_name' : 'fleet_name';
|
||||
const tName = this.useAppWord ? 'APPLICATION NAME' : 'FLEET';
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
options.json
|
||||
? `application_name => ${jName}`
|
||||
: `application_name => ${tName}`,
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
@ -156,9 +117,6 @@ export default class DevicesCmd extends Command {
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetOutputMsg));
|
||||
}
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
|
@ -21,33 +21,18 @@ 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 { isV13 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
discontinued: boolean;
|
||||
help: void;
|
||||
json?: boolean;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
const deprecatedInfo = isV13()
|
||||
? ''
|
||||
: `
|
||||
The --verbose option may add extra columns/fields to the output. Currently
|
||||
this includes the "STATE" column which is DEPRECATED and whose values are one
|
||||
of 'new', 'released' or 'discontinued'. However, 'discontinued' device types
|
||||
are only listed if the '--discontinued' option is also used, and this option
|
||||
is also DEPRECATED.
|
||||
`
|
||||
.split('\n')
|
||||
.join(`\n\t\t`);
|
||||
|
||||
export default class DevicesSupportedCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
${deprecatedInfo}
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings (for example, the ALIASES column contains a
|
||||
@ -56,8 +41,7 @@ export default class DevicesSupportedCmd extends Command {
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices supported',
|
||||
'$ balena devices supported --verbose',
|
||||
'$ balena devices supported -vj',
|
||||
'$ balena devices supported --json',
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
@ -66,22 +50,11 @@ export default class DevicesSupportedCmd extends Command {
|
||||
).trim();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
discontinued: flags.boolean({
|
||||
description: isV13()
|
||||
? 'No effect (DEPRECATED)'
|
||||
: 'include "discontinued" device types (DEPRECATED)',
|
||||
}),
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
verbose: flags.boolean({
|
||||
char: 'v',
|
||||
description: isV13()
|
||||
? 'No effect (DEPRECATED)'
|
||||
: 'add extra columns in the tabular output (DEPRECATED)',
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
@ -95,53 +68,28 @@ export default class DevicesSupportedCmd extends Command {
|
||||
]);
|
||||
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
|
||||
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
|
||||
const discontinuedDTs = isV13()
|
||||
? []
|
||||
: configDTs.filter((dt) => dt.state === 'DISCONTINUED');
|
||||
const discontinuedDTsBySlug = _.keyBy(discontinuedDTs, (dt) => dt.slug);
|
||||
// set of slugs from models.deviceType.getAllSupported() plus slugs of
|
||||
// discontinued device types as per models.config.getDeviceTypes()
|
||||
const slugsOfInterest = new Set([
|
||||
...Object.keys(dtsBySlug),
|
||||
...Object.keys(discontinuedDTsBySlug),
|
||||
]);
|
||||
interface DT {
|
||||
slug: string;
|
||||
aliases: string[];
|
||||
arch: string;
|
||||
state?: string; // to be removed in CLI v13
|
||||
name: string;
|
||||
}
|
||||
let deviceTypes: DT[] = [];
|
||||
for (const slug of slugsOfInterest) {
|
||||
for (const slug of Object.keys(dtsBySlug)) {
|
||||
const configDT: Partial<typeof configDTs[0]> =
|
||||
configDTsBySlug[slug] || {};
|
||||
if (configDT.state === 'DISCONTINUED' && !options.discontinued) {
|
||||
continue;
|
||||
}
|
||||
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
|
||||
const aliases = (configDT.aliases || []).filter(
|
||||
(alias) => alias !== slug,
|
||||
);
|
||||
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
|
||||
deviceTypes.push({
|
||||
slug,
|
||||
aliases: options.json ? aliases : [aliases.join(', ')],
|
||||
arch:
|
||||
(dt.is_of__cpu_architecture as any)?.[0]?.slug ||
|
||||
configDT.arch ||
|
||||
'n/a',
|
||||
// 'BETA' renamed to 'NEW'
|
||||
// https://www.flowdock.com/app/rulemotion/i-cli/threads/1svvyaf8FAZeSdG4dPJc4kHOvJU
|
||||
state: isV13()
|
||||
? undefined
|
||||
: (configDT.state || 'NEW').replace('BETA', 'NEW'),
|
||||
name: dt.name || configDT.name || 'N/A',
|
||||
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
|
||||
name: dt.name || 'N/A',
|
||||
});
|
||||
}
|
||||
const fields =
|
||||
options.verbose && !isV13()
|
||||
? ['slug', 'aliases', 'arch', 'state', 'name']
|
||||
: ['slug', 'aliases', 'arch', 'name'];
|
||||
const fields = ['slug', 'aliases', 'arch', 'name'];
|
||||
deviceTypes = _.sortBy(deviceTypes, fields);
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
|
32
lib/commands/env/add.ts
vendored
32
lib/commands/env/add.ts
vendored
@ -21,15 +21,9 @@ import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string; // device UUID
|
||||
help: void;
|
||||
@ -101,11 +95,8 @@ export default class EnvAddCmd extends Command {
|
||||
public static usage = 'env add <name> [value]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13()
|
||||
? {}
|
||||
: { application: { ...cf.application, exclusive: ['fleet', 'device'] } }),
|
||||
fleet: { ...cf.fleet, exclusive: ['application', 'device'] },
|
||||
device: { ...cf.device, exclusive: ['application', 'fleet'] },
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
device: { ...cf.device, exclusive: ['fleet'] },
|
||||
help: cf.help,
|
||||
quiet: cf.quiet,
|
||||
service: cf.service,
|
||||
@ -117,11 +108,7 @@ export default class EnvAddCmd extends Command {
|
||||
);
|
||||
const cmd = this;
|
||||
|
||||
if (options.application && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.fleet;
|
||||
if (!options.application && !options.device) {
|
||||
if (!options.fleet && !options.device) {
|
||||
throw new ExpectedError(
|
||||
'Either the --fleet or the --device option must be specified',
|
||||
);
|
||||
@ -163,11 +150,12 @@ export default class EnvAddCmd extends Command {
|
||||
}
|
||||
|
||||
const varType = isConfigVar ? 'configVar' : 'envVar';
|
||||
if (options.application) {
|
||||
for (const app of options.application.split(',')) {
|
||||
if (options.fleet) {
|
||||
const { getFleetSlug } = await import('../../utils/sdk');
|
||||
for (const app of options.fleet.split(',')) {
|
||||
try {
|
||||
await balena.models.application[varType].set(
|
||||
app,
|
||||
await getFleetSlug(balena, app),
|
||||
params.name,
|
||||
params.value,
|
||||
);
|
||||
@ -201,8 +189,8 @@ async function setServiceVars(
|
||||
params: ArgsDef,
|
||||
options: FlagsDef,
|
||||
) {
|
||||
if (options.application) {
|
||||
for (const app of options.application.split(',')) {
|
||||
if (options.fleet) {
|
||||
for (const app of options.fleet.split(',')) {
|
||||
for (const service of options.service!.split(',')) {
|
||||
try {
|
||||
const serviceId = await getServiceIdForApp(sdk, app, service);
|
||||
|
@ -21,42 +21,33 @@ import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
appToFleetOutputMsg,
|
||||
warnify,
|
||||
} from '../utils/messages';
|
||||
import { isV13 } from '../utils/version';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
config: boolean;
|
||||
device?: string; // device UUID
|
||||
json: boolean;
|
||||
help: void;
|
||||
service?: string; // service name
|
||||
verbose: boolean;
|
||||
v13: boolean;
|
||||
}
|
||||
|
||||
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
|
||||
appName?: string | null; // application name
|
||||
fleet?: string | null; // fleet slug
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
interface DeviceServiceEnvironmentVariableInfo
|
||||
extends SDK.DeviceServiceEnvironmentVariable {
|
||||
appName?: string; // application name
|
||||
fleet?: string; // fleet slug
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
interface ServiceEnvironmentVariableInfo
|
||||
extends SDK.ServiceEnvironmentVariable {
|
||||
appName?: string; // application name
|
||||
fleet?: string; // fleet slug
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
@ -96,8 +87,6 @@ export default class EnvsCmd extends Command {
|
||||
in case the current user was removed from the fleet by the fleet's owner).
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
${appToFleetOutputMsg.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
@ -115,57 +104,35 @@ export default class EnvsCmd extends Command {
|
||||
public static usage = 'envs';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
all: flags.boolean({
|
||||
default: false,
|
||||
description: 'No-op since balena CLI v12.0.0.',
|
||||
hidden: true,
|
||||
}),
|
||||
application: {
|
||||
exclusive: ['device', 'fleet', 'v13'],
|
||||
...cf.application,
|
||||
},
|
||||
}),
|
||||
fleet: { exclusive: ['device', 'application'], ...cf.fleet },
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
config: flags.boolean({
|
||||
default: false,
|
||||
char: 'c',
|
||||
description: 'show configuration variables only',
|
||||
exclusive: ['service'],
|
||||
}),
|
||||
device: { exclusive: ['fleet', 'application'], ...cf.device },
|
||||
device: { ...cf.device, exclusive: ['fleet'] },
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
verbose: cf.verbose,
|
||||
service: { exclusive: ['config'], ...cf.service },
|
||||
v13: cf.v13,
|
||||
service: { ...cf.service, exclusive: ['config'] },
|
||||
};
|
||||
|
||||
protected useAppWord = false;
|
||||
protected hasWarned = false;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
|
||||
this.useAppWord = !options.fleet && !options.v13 && !isV13();
|
||||
|
||||
const variables: EnvironmentVariableInfo[] = [];
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
if (options.application && !options.json && process.stderr.isTTY) {
|
||||
this.hasWarned = true;
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.fleet;
|
||||
if (!options.application && !options.device) {
|
||||
if (!options.fleet && !options.device) {
|
||||
throw new ExpectedError('Missing --fleet or --device option');
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
let appNameOrSlug = options.application;
|
||||
let fleetSlug: string | undefined = options.fleet
|
||||
? await (await import('../utils/sdk')).getFleetSlug(balena, options.fleet)
|
||||
: undefined;
|
||||
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
|
||||
|
||||
if (options.device) {
|
||||
@ -178,23 +145,23 @@ export default class EnvsCmd extends Command {
|
||||
);
|
||||
fullUUID = device.uuid;
|
||||
if (app) {
|
||||
appNameOrSlug = app.slug;
|
||||
fleetSlug = app.slug;
|
||||
}
|
||||
}
|
||||
if (appNameOrSlug && options.service) {
|
||||
await validateServiceName(balena, options.service, appNameOrSlug);
|
||||
if (fleetSlug && options.service) {
|
||||
await validateServiceName(balena, options.service, fleetSlug);
|
||||
}
|
||||
variables.push(...(await getAppVars(balena, appNameOrSlug, options)));
|
||||
variables.push(...(await getAppVars(balena, fleetSlug, options)));
|
||||
if (fullUUID) {
|
||||
variables.push(
|
||||
...(await getDeviceVars(balena, fullUUID, appNameOrSlug, options)),
|
||||
...(await getDeviceVars(balena, fullUUID, fleetSlug, options)),
|
||||
);
|
||||
}
|
||||
if (!options.json && variables.length === 0) {
|
||||
const target =
|
||||
(options.service ? `service "${options.service}" of ` : '') +
|
||||
(options.application
|
||||
? `fleet "${options.application}"`
|
||||
(options.fleet
|
||||
? `fleet "${options.fleet}"`
|
||||
: `device "${options.device}"`);
|
||||
throw new ExpectedError(`No environment variables found for ${target}`);
|
||||
}
|
||||
@ -206,24 +173,14 @@ export default class EnvsCmd extends Command {
|
||||
varArray: EnvironmentVariableInfo[],
|
||||
options: FlagsDef,
|
||||
) {
|
||||
const fields = ['id', 'name', 'value'];
|
||||
const fields = ['id', 'name', 'value', 'fleet'];
|
||||
|
||||
// Replace undefined app names with 'N/A' or null
|
||||
varArray = varArray.map((i: EnvironmentVariableInfo) => {
|
||||
if (i.appName) {
|
||||
// use slug in v13, app name in v12 for compatibility
|
||||
i.appName = isV13()
|
||||
? i.appName
|
||||
: i.appName.substring(i.appName.indexOf('/') + 1);
|
||||
} else {
|
||||
i.appName = options.json ? null : 'N/A';
|
||||
}
|
||||
i.fleet ||= options.json ? null : 'N/A';
|
||||
return i;
|
||||
});
|
||||
|
||||
const jName = this.useAppWord ? 'appName' : 'fleetName';
|
||||
const tName = this.useAppWord ? 'APPLICATION' : 'FLEET';
|
||||
fields.push(options.json ? `appName => ${jName}` : `appName => ${tName}`);
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
@ -236,9 +193,6 @@ export default class EnvsCmd extends Command {
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetOutputMsg));
|
||||
}
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
@ -252,14 +206,14 @@ export default class EnvsCmd extends Command {
|
||||
async function validateServiceName(
|
||||
sdk: SDK.BalenaSDK,
|
||||
serviceName: string,
|
||||
appName: string,
|
||||
fleetSlug: string,
|
||||
) {
|
||||
const services = await sdk.models.service.getAllByApplication(appName, {
|
||||
const services = await sdk.models.service.getAllByApplication(fleetSlug, {
|
||||
$filter: { service_name: serviceName },
|
||||
});
|
||||
if (services.length === 0) {
|
||||
throw new ExpectedError(
|
||||
`Service "${serviceName}" not found for fleet "${appName}"`,
|
||||
`Service "${serviceName}" not found for fleet "${fleetSlug}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -273,17 +227,17 @@ async function validateServiceName(
|
||||
*/
|
||||
async function getAppVars(
|
||||
sdk: SDK.BalenaSDK,
|
||||
appNameOrSlug: string | undefined,
|
||||
fleetSlug: string | undefined,
|
||||
options: FlagsDef,
|
||||
): Promise<EnvironmentVariableInfo[]> {
|
||||
const appVars: EnvironmentVariableInfo[] = [];
|
||||
if (!appNameOrSlug) {
|
||||
if (!fleetSlug) {
|
||||
return appVars;
|
||||
}
|
||||
const vars = await sdk.models.application[
|
||||
options.config ? 'configVar' : 'envVar'
|
||||
].getAllByApplication(appNameOrSlug);
|
||||
fillInInfoFields(vars, appNameOrSlug);
|
||||
].getAllByApplication(fleetSlug);
|
||||
fillInInfoFields(vars, fleetSlug);
|
||||
appVars.push(...vars);
|
||||
if (!options.config) {
|
||||
const pineOpts: SDK.PineOptions<SDK.ServiceEnvironmentVariable> = {
|
||||
@ -299,10 +253,10 @@ async function getAppVars(
|
||||
};
|
||||
}
|
||||
const serviceVars = await sdk.models.service.var.getAllByApplication(
|
||||
appNameOrSlug,
|
||||
fleetSlug,
|
||||
pineOpts,
|
||||
);
|
||||
fillInInfoFields(serviceVars, appNameOrSlug);
|
||||
fillInInfoFields(serviceVars, fleetSlug);
|
||||
appVars.push(...serviceVars);
|
||||
}
|
||||
return appVars;
|
||||
@ -315,7 +269,7 @@ async function getAppVars(
|
||||
async function getDeviceVars(
|
||||
sdk: SDK.BalenaSDK,
|
||||
fullUUID: string,
|
||||
appNameOrSlug: string | undefined,
|
||||
fleetSlug: string | undefined,
|
||||
options: FlagsDef,
|
||||
): Promise<EnvironmentVariableInfo[]> {
|
||||
const printedUUID = options.json ? fullUUID : options.device!;
|
||||
@ -324,7 +278,7 @@ async function getDeviceVars(
|
||||
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
|
||||
fullUUID,
|
||||
);
|
||||
fillInInfoFields(deviceConfigVars, appNameOrSlug, printedUUID);
|
||||
fillInInfoFields(deviceConfigVars, fleetSlug, printedUUID);
|
||||
deviceVars.push(...deviceConfigVars);
|
||||
} else {
|
||||
const pineOpts: SDK.PineOptions<SDK.DeviceServiceEnvironmentVariable> = {
|
||||
@ -345,13 +299,13 @@ async function getDeviceVars(
|
||||
fullUUID,
|
||||
pineOpts,
|
||||
);
|
||||
fillInInfoFields(deviceServiceVars, appNameOrSlug, printedUUID);
|
||||
fillInInfoFields(deviceServiceVars, fleetSlug, printedUUID);
|
||||
deviceVars.push(...deviceServiceVars);
|
||||
|
||||
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
|
||||
fullUUID,
|
||||
);
|
||||
fillInInfoFields(deviceEnvVars, appNameOrSlug, printedUUID);
|
||||
fillInInfoFields(deviceEnvVars, fleetSlug, printedUUID);
|
||||
deviceVars.push(...deviceEnvVars);
|
||||
}
|
||||
return deviceVars;
|
||||
@ -367,7 +321,7 @@ function fillInInfoFields(
|
||||
| EnvironmentVariableInfo[]
|
||||
| DeviceServiceEnvironmentVariableInfo[]
|
||||
| ServiceEnvironmentVariableInfo[],
|
||||
appNameOrSlug?: string,
|
||||
fleetSlug?: string,
|
||||
deviceUUID?: string,
|
||||
) {
|
||||
for (const envVar of varArray) {
|
||||
@ -381,7 +335,7 @@ function fillInInfoFields(
|
||||
?.installs__service as SDK.Service[]
|
||||
)[0]?.service_name;
|
||||
}
|
||||
envVar.appName = appNameOrSlug;
|
||||
envVar.fleet = fleetSlug;
|
||||
envVar.serviceName = envVar.serviceName || '*';
|
||||
envVar.deviceUUID = deviceUUID || '*';
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,6 +15,135 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FleetCreateCmd } from '../app/create';
|
||||
import { flags } from '@oclif/command';
|
||||
import type { Application } from 'balena-sdk';
|
||||
|
||||
export default FleetCreateCmd;
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class FleetCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create a fleet.
|
||||
|
||||
Create a new balena fleet.
|
||||
|
||||
You can specify the organization the fleet should belong to using
|
||||
the \`--organization\` option. The organization's handle, not its name,
|
||||
should be provided. Organization handles can be listed with the
|
||||
\`balena orgs\` command.
|
||||
|
||||
The fleet's default device type is specified with the \`--type\` option.
|
||||
The \`balena devices supported\` command can be used to list the available
|
||||
device types.
|
||||
|
||||
Interactive dropdowns will be shown for selection if no device type or
|
||||
organization is specified and there are multiple options to choose from.
|
||||
If there is a single option to choose from, it will be chosen automatically.
|
||||
This interactive behavior can be disabled by explicitly specifying a device
|
||||
type and organization.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet create MyFleet',
|
||||
'$ balena fleet create MyFleet --organization mmyorg',
|
||||
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'fleet name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'fleet create <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
organization: flags.string({
|
||||
char: 'o',
|
||||
description: 'handle of the organization the fleet should belong to',
|
||||
}),
|
||||
type: flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'fleet device type (Check available types with `balena devices supported`)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
FleetCreateCmd,
|
||||
);
|
||||
|
||||
// Ascertain device type
|
||||
const deviceType =
|
||||
options.type ||
|
||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
||||
|
||||
// Ascertain organization
|
||||
const organization =
|
||||
options.organization?.toLowerCase() || (await this.getOrganization());
|
||||
|
||||
// Create application
|
||||
let application: Application;
|
||||
try {
|
||||
application = await getBalenaSdk().models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
organization,
|
||||
});
|
||||
} catch (err) {
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
throw new ExpectedError(
|
||||
`Error: fleet "${params.name}" already exists in organization "${organization}".`,
|
||||
);
|
||||
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
|
||||
// BalenaRequestError: Request error: Unauthorized
|
||||
throw new ExpectedError(
|
||||
`Error: You are not authorized to create fleets in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Output
|
||||
console.log(
|
||||
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
|
||||
);
|
||||
}
|
||||
|
||||
async getOrganization() {
|
||||
const { getOwnOrganizations } = await import('../../utils/sdk');
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk());
|
||||
|
||||
if (organizations.length === 0) {
|
||||
// User is not a member of any organizations (should not happen).
|
||||
throw new Error('This account is not a member of any organizations');
|
||||
} else if (organizations.length === 1) {
|
||||
// User is a member of only one organization - use this.
|
||||
return organizations[0].handle;
|
||||
} else {
|
||||
// User is a member of multiple organizations -
|
||||
const { selectOrganization } = await import('../../utils/patterns');
|
||||
return selectOrganization(organizations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,6 +15,89 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FleetCmd } from '../app';
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { Release } from 'balena-sdk';
|
||||
|
||||
export default FleetCmd;
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import { isV14 } from '../../utils/version';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class FleetCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display information about a single fleet.
|
||||
|
||||
Display detailed information about a single fleet.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena fleet MyFleet',
|
||||
'$ balena fleet myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet <fleet>';
|
||||
|
||||
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, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
FleetCmd,
|
||||
);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const application = (await getApplication(getBalenaSdk(), params.fleet, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
},
|
||||
})) as ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
// For display purposes:
|
||||
device_type: string;
|
||||
commit?: string;
|
||||
};
|
||||
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
application.commit = application.should_be_running__release[0]?.commit;
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(
|
||||
application,
|
||||
['app_name', 'id', 'device_type', 'slug', 'commit'],
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
||||
console.log(`== ${application.slug}`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(application, [
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
* Copyright 2016-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.
|
||||
@ -15,6 +15,67 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FleetPurgeCmd } from '../app/purge';
|
||||
import type { flags } from '@oclif/command';
|
||||
|
||||
export default FleetPurgeCmd;
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class FleetPurgeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Purge data from a fleet.
|
||||
|
||||
Purge data from all devices belonging to a fleet.
|
||||
This will clear the fleet's '/data' directory.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet purge MyFleet',
|
||||
'$ balena fleet purge myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet purge <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// balena.models.application.purge only accepts a numeric id
|
||||
// so we must first fetch the app to get it's id,
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
try {
|
||||
await balena.models.application.purge(application.id);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('no online device(s) found')) {
|
||||
// application.purge throws an error if no devices are online
|
||||
// ignore in this case.
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
* Copyright 2016-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.
|
||||
@ -15,6 +15,135 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FleetRenameCmd } from '../app/rename';
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { ApplicationType } from 'balena-sdk';
|
||||
|
||||
export default FleetRenameCmd;
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
newName?: string;
|
||||
}
|
||||
|
||||
export default class FleetRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Rename a fleet.
|
||||
|
||||
Rename a fleet.
|
||||
|
||||
Note, if the \`newName\` parameter is omitted, it will be
|
||||
prompted for interactively.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet rename OldName',
|
||||
'$ balena fleet rename OldName NewName',
|
||||
'$ balena fleet rename myorg/oldname NewName',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
ca.fleetRequired,
|
||||
{
|
||||
name: 'newName',
|
||||
description: 'the new name for the fleet',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'fleet rename <fleet> [newName]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
|
||||
|
||||
const { validateApplicationName } = await import('../../utils/validation');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: ['is_legacy'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check app exists
|
||||
if (!application) {
|
||||
throw new ExpectedError(`Error: fleet ${params.fleet} not found.`);
|
||||
}
|
||||
|
||||
// Check app supports renaming
|
||||
const appType = (application.application_type as ApplicationType[])?.[0];
|
||||
if (appType.is_legacy) {
|
||||
throw new ExpectedError(
|
||||
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Ascertain new name
|
||||
const newName =
|
||||
params.newName ||
|
||||
(await getCliForm().ask({
|
||||
message: 'Please enter the new name for this fleet:',
|
||||
type: 'input',
|
||||
validate: validateApplicationName,
|
||||
})) ||
|
||||
'';
|
||||
|
||||
// Check they haven't used slug in new name
|
||||
if (newName.includes('/')) {
|
||||
throw new ExpectedError(
|
||||
`New fleet name cannot include '/', please check that you are not specifying fleet slug.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Rename
|
||||
try {
|
||||
await balena.models.application.rename(application.id, newName);
|
||||
} catch (e) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
if ((e.message || '').toLowerCase().includes('unique')) {
|
||||
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
|
||||
}
|
||||
// BalenaRequestError: Request error: App name may only contain [a-zA-Z0-9_-].
|
||||
if ((e.message || '').toLowerCase().includes('name may only contain')) {
|
||||
throw new ExpectedError(
|
||||
`Error: new fleet name may only include characters [a-zA-Z0-9_-].`,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Get application again, to be sure of results
|
||||
const renamedApplication = await balena.models.application.get(
|
||||
application.id,
|
||||
);
|
||||
|
||||
// Output result
|
||||
console.log(`Fleet renamed`);
|
||||
console.log('From:');
|
||||
console.log(`\tname: ${application.app_name}`);
|
||||
console.log(`\tslug: ${application.slug}`);
|
||||
console.log('To:');
|
||||
console.log(`\tname: ${renamedApplication.app_name}`);
|
||||
console.log(`\tslug: ${renamedApplication.slug}`);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
* Copyright 2016-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.
|
||||
@ -15,6 +15,56 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FleetRestartCmd } from '../app/restart';
|
||||
import type { flags } from '@oclif/command';
|
||||
|
||||
export default FleetRestartCmd;
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class FleetRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart a fleet.
|
||||
|
||||
Restart all devices belonging to a fleet.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet restart MyFleet',
|
||||
'$ balena fleet restart myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet restart <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRestartCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
await balena.models.application.restart(application.id);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
* Copyright 2016-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.
|
||||
@ -15,6 +15,70 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FleetRmCmd } from '../app/rm';
|
||||
import type { flags } from '@oclif/command';
|
||||
|
||||
export default FleetRmCmd;
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class FleetRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a fleet.
|
||||
|
||||
Permanently remove a fleet.
|
||||
|
||||
The --yes option may be used to avoid interactive confirmation.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet rm MyFleet',
|
||||
'$ balena fleet rm MyFleet --yes',
|
||||
'$ balena fleet rm myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet rm <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
FleetRmCmd,
|
||||
);
|
||||
|
||||
const { confirm } = await import('../../utils/patterns');
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Confirm
|
||||
await confirm(
|
||||
options.yes ?? false,
|
||||
`Are you sure you want to delete fleet ${params.fleet}?`,
|
||||
);
|
||||
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
// Remove
|
||||
await balena.models.application.remove(application.id);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,94 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FleetsCmd } from './apps';
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
export default FleetsCmd;
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { isV14 } from '../utils/version';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
device_count: number;
|
||||
online_devices: number;
|
||||
device_type?: string;
|
||||
}
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export default class FleetsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all fleets.
|
||||
|
||||
List all your balena fleets.
|
||||
|
||||
For detailed information on a particular fleet, use
|
||||
\`balena fleet <fleet>\`
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena fleets'];
|
||||
|
||||
public static usage = 'fleets';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(FleetsCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Get applications
|
||||
const applications =
|
||||
(await balena.models.application.getAllDirectlyAccessible({
|
||||
$select: ['id', 'app_name', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
owns__device: { $select: 'is_online' },
|
||||
},
|
||||
})) as ExtendedApplication[];
|
||||
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
application.device_count = application.owns__device?.length ?? 0;
|
||||
application.online_devices =
|
||||
application.owns__device?.filter((d) => d.is_online).length || 0;
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
});
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(
|
||||
applications,
|
||||
[
|
||||
'id',
|
||||
'app_name',
|
||||
'slug',
|
||||
'device_type',
|
||||
'device_count',
|
||||
'online_devices',
|
||||
],
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
'app_name => NAME',
|
||||
'slug',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,10 +21,8 @@ import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import { parseAsLocalHostnameOrIp } from '../utils/validation';
|
||||
import { isV13 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
pollInterval?: number;
|
||||
help?: void;
|
||||
@ -77,7 +75,6 @@ export default class JoinCmd extends Command {
|
||||
public static usage = 'join [deviceIpOrHostname]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13() ? {} : { application: cf.application }),
|
||||
fleet: cf.fleet,
|
||||
pollInterval: flags.integer({
|
||||
description: 'the interval in minutes to check for updates',
|
||||
@ -101,7 +98,7 @@ export default class JoinCmd extends Command {
|
||||
logger,
|
||||
sdk,
|
||||
params.deviceIpOrHostname,
|
||||
options.application || options.fleet,
|
||||
options.fleet,
|
||||
options.pollInterval,
|
||||
);
|
||||
}
|
||||
|
@ -113,6 +113,12 @@ export default class LocalConfigureCmd extends Command {
|
||||
},
|
||||
domain: [['config_json', 'hostname']],
|
||||
},
|
||||
{
|
||||
template: {
|
||||
developmentMode: '{{developmentMode}}',
|
||||
},
|
||||
domain: [['config_json', 'developmentMode']],
|
||||
},
|
||||
{
|
||||
template: {
|
||||
wifi: {
|
||||
@ -163,6 +169,13 @@ export default class LocalConfigureCmd extends Command {
|
||||
name: 'networkKey',
|
||||
default: data.networkKey,
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Enable development mode? (Open ports and root access - Not for production!)',
|
||||
type: 'confirm',
|
||||
name: 'developmentMode',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
message: 'Do you want to set advanced settings?',
|
||||
type: 'confirm',
|
||||
|
@ -35,10 +35,10 @@ interface ArgsDef {
|
||||
|
||||
export default class OsBuildConfigCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Build an OS config and save it to a JSON file.
|
||||
Prepare a configuration file for use by the 'os configure' command.
|
||||
|
||||
Interactively generate an OS config once, so that the generated config
|
||||
file can be used in \`balena os configure\`, skipping the interactive part.
|
||||
Interactively generate a configuration file that can then be used as
|
||||
non-interactive input by the 'balena os configure' command.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
|
@ -23,27 +23,20 @@ import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
|
||||
|
||||
const CONNECTIONS_FOLDER = '/system-connections';
|
||||
|
||||
interface FlagsDef {
|
||||
advanced?: boolean;
|
||||
application?: string;
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
config?: string;
|
||||
'config-app-update-poll-interval'?: number;
|
||||
'config-network'?: string;
|
||||
'config-wifi-key'?: string;
|
||||
'config-wifi-ssid'?: string;
|
||||
dev?: boolean; // balenaOS development variant
|
||||
device?: string; // device UUID
|
||||
'device-api-key'?: string;
|
||||
'device-type'?: string;
|
||||
help?: void;
|
||||
version?: string;
|
||||
@ -58,6 +51,7 @@ interface ArgsDef {
|
||||
|
||||
interface Answers {
|
||||
appUpdatePollInterval: number; // in minutes
|
||||
developmentMode?: boolean; // balenaOS development variant
|
||||
deviceType: string; // e.g. "raspberrypi3"
|
||||
network: 'ethernet' | 'wifi';
|
||||
version: string; // e.g. "2.32.0+rev1"
|
||||
@ -66,10 +60,6 @@ interface Answers {
|
||||
provisioningKeyName?: string;
|
||||
}
|
||||
|
||||
const deviceApiKeyDeprecationMsg = stripIndent`
|
||||
The --device-api-key option is deprecated and will be removed in a future release.
|
||||
A suitable key is automatically generated or fetched if this option is omitted.`;
|
||||
|
||||
export default class OsConfigureCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Configure a previously downloaded balenaOS image.
|
||||
@ -83,18 +73,18 @@ export default class OsConfigureCmd extends Command {
|
||||
2. A given \`config.json\` file specified with the \`--config\` option.
|
||||
3. User input through interactive prompts (text menus).
|
||||
|
||||
The --device-type option may be used to override the fleet's default device
|
||||
type, in case of a fleet with mixed device types.
|
||||
The --device-type option is used to override the fleet's default device type,
|
||||
in case of a fleet with mixed device types.
|
||||
|
||||
The --system-connection (-c) option can be used to inject NetworkManager connection
|
||||
${devModeInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
The --system-connection (-c) option is used to inject NetworkManager connection
|
||||
profiles for additional network interfaces, such as cellular/GSM or additional
|
||||
WiFi or ethernet connections. This option may be passed multiple times in case there
|
||||
are multiple files to inject. See connection profile examples and reference at:
|
||||
https://www.balena.io/docs/reference/OS/network/2.x/
|
||||
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
|
||||
|
||||
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
Note: This command is currently not supported on Windows natively. Windows users
|
||||
@ -105,7 +95,6 @@ export default class OsConfigureCmd extends Command {
|
||||
|
||||
public static examples = [
|
||||
'$ balena os configure ../path/rpi3.img --device 7cf02a6',
|
||||
'$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>',
|
||||
'$ balena os configure ../path/rpi3.img --fleet myorg/myfleet',
|
||||
'$ balena os configure ../path/rpi3.img --fleet MyFleet --version 2.12.7',
|
||||
'$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3',
|
||||
@ -128,22 +117,7 @@ export default class OsConfigureCmd extends Command {
|
||||
description:
|
||||
'ask advanced configuration questions (when in interactive mode)',
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'fleet', 'device'],
|
||||
},
|
||||
}),
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
},
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
config: flags.string({
|
||||
description:
|
||||
'path to a pre-generated config.json file to be injected in the OS image',
|
||||
@ -163,15 +137,8 @@ export default class OsConfigureCmd extends Command {
|
||||
'config-wifi-ssid': flags.string({
|
||||
description: 'WiFi SSID (network name) (non-interactive configuration)',
|
||||
}),
|
||||
device: {
|
||||
exclusive: ['app', 'application', 'fleet', 'provisioning-key-name'],
|
||||
...cf.device,
|
||||
},
|
||||
'device-api-key': flags.string({
|
||||
char: 'k',
|
||||
description:
|
||||
'custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)',
|
||||
}),
|
||||
dev: cf.dev,
|
||||
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',
|
||||
@ -203,10 +170,6 @@ export default class OsConfigureCmd extends Command {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
OsConfigureCmd,
|
||||
);
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
await validateOptions(options);
|
||||
|
||||
@ -233,7 +196,7 @@ export default class OsConfigureCmd extends Command {
|
||||
};
|
||||
deviceTypeSlug = device.is_of__device_type[0].slug;
|
||||
} else {
|
||||
app = (await getApplication(balena, options.application!, {
|
||||
app = (await getApplication(balena, options.fleet!, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
@ -254,27 +217,28 @@ export default class OsConfigureCmd extends Command {
|
||||
configJson = JSON.parse(rawConfig);
|
||||
}
|
||||
|
||||
const osVersion =
|
||||
options.version ||
|
||||
(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit));
|
||||
|
||||
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
||||
await validateDevOptionAndWarn(options.dev, osVersion);
|
||||
|
||||
const answers: Answers = await askQuestionsForDeviceType(
|
||||
deviceTypeManifest,
|
||||
options,
|
||||
configJson,
|
||||
);
|
||||
if (options.application) {
|
||||
if (options.fleet) {
|
||||
answers.deviceType = deviceTypeSlug;
|
||||
}
|
||||
answers.version =
|
||||
options.version ||
|
||||
(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit));
|
||||
|
||||
answers.version = osVersion;
|
||||
answers.developmentMode = options.dev;
|
||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||
|
||||
if (_.isEmpty(configJson)) {
|
||||
if (device) {
|
||||
configJson = await generateDeviceConfig(
|
||||
device,
|
||||
options['device-api-key'],
|
||||
answers,
|
||||
);
|
||||
configJson = await generateDeviceConfig(device, undefined, answers);
|
||||
} else {
|
||||
configJson = await generateApplicationConfig(app!, answers);
|
||||
}
|
||||
@ -335,23 +299,16 @@ export default class OsConfigureCmd extends Command {
|
||||
async function validateOptions(options: FlagsDef) {
|
||||
// The 'device' and 'application' options are declared "exclusive" in the oclif
|
||||
// flag definitions above, so oclif will enforce that they are not both used together.
|
||||
if (!options.device && !options.application) {
|
||||
if (!options.device && !options.fleet) {
|
||||
throw new ExpectedError(
|
||||
"Either the '--device' or the '--fleet' option must be provided",
|
||||
);
|
||||
}
|
||||
if (!options.application && options['device-type']) {
|
||||
if (!options.fleet && options['device-type']) {
|
||||
throw new ExpectedError(
|
||||
"The '--device-type' option can only be used in conjunction with the '--fleet' option",
|
||||
);
|
||||
}
|
||||
if (options['device-api-key']) {
|
||||
console.error(stripIndent`
|
||||
-------------------------------------------------------------------------------------------
|
||||
Warning: ${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t\t')}
|
||||
-------------------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
}
|
||||
@ -401,7 +358,7 @@ async function checkDeviceTypeCompatibility(
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
|
||||
throw new ExpectedError(
|
||||
`Device type ${options['device-type']} is incompatible with fleet ${options.application}`,
|
||||
`Device type ${options['device-type']} is incompatible with fleet ${options.fleet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -426,7 +383,13 @@ async function askQuestionsForDeviceType(
|
||||
options: FlagsDef,
|
||||
configJson?: import('../../utils/config').ImgConfig,
|
||||
): Promise<Answers> {
|
||||
const answerSources: any[] = [camelifyConfigOptions(options)];
|
||||
const answerSources: any[] = [
|
||||
{
|
||||
...camelifyConfigOptions(options),
|
||||
app: options.fleet,
|
||||
application: options.fleet,
|
||||
},
|
||||
];
|
||||
const defaultAnswers: Partial<Answers> = {};
|
||||
const questions: any = deviceType.options;
|
||||
let extraOpts: { override: object } | undefined;
|
||||
|
@ -56,9 +56,11 @@ export default class OsDownloadCmd extends Command {
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2021.10.2.prod',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu-esr',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -96,12 +98,48 @@ export default class OsDownloadCmd extends Command {
|
||||
OsDownloadCmd,
|
||||
);
|
||||
|
||||
// balenaOS ESR versions require user authentication
|
||||
if (options.version) {
|
||||
const { isESR } = await import('balena-image-manager');
|
||||
if (options.version === 'menu-esr' || isESR(options.version)) {
|
||||
try {
|
||||
await OsDownloadCmd.checkLoggedIn();
|
||||
} catch (e) {
|
||||
const { ExpectedError, NotLoggedInError } = await import(
|
||||
'../../errors'
|
||||
);
|
||||
if (e instanceof NotLoggedInError) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
${e.message}
|
||||
User authentication is required to download balenaOS ESR versions.`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { downloadOSImage } = await import('../../utils/cloud');
|
||||
|
||||
try {
|
||||
await downloadOSImage(params.type, options.output, options.version);
|
||||
} catch (e) {
|
||||
e.deviceTypeSlug = params.type;
|
||||
e.message ||= '';
|
||||
if (
|
||||
e.code === 'BalenaRequestError' ||
|
||||
e.message.toLowerCase().includes('no such version')
|
||||
) {
|
||||
const version = options.version || '';
|
||||
if (
|
||||
!version.endsWith('.dev') &&
|
||||
!version.endsWith('.prod') &&
|
||||
/^v?\d+\.\d+\.\d+/.test(version)
|
||||
) {
|
||||
e.message += `
|
||||
** Hint: some OS releases require specifying the full OS version including
|
||||
** the '.prod' or '.dev' suffix, e.g. '--version 2021.10.2.prod'`;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -65,9 +65,11 @@ export default class OsVersionsCmd extends Command {
|
||||
OsVersionsCmd,
|
||||
);
|
||||
|
||||
const { getFormattedOsVersions } = await import('../../utils/cloud');
|
||||
const vs = await getFormattedOsVersions(params.type, !!options.esr);
|
||||
const { formatOsVersion, getOsVersions } = await import(
|
||||
'../../utils/cloud'
|
||||
);
|
||||
const vs = await getOsVersions(params.type, !!options.esr);
|
||||
|
||||
console.log(vs.map((v) => v.formattedVersion).join('\n'));
|
||||
console.log(vs.map((v) => formatOsVersion(v)).join('\n'));
|
||||
}
|
||||
}
|
||||
|
@ -24,15 +24,10 @@ import {
|
||||
getVisuals,
|
||||
stripIndent,
|
||||
} from '../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../utils/messages';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import type { DockerConnectionCliFlags } from '../utils/docker';
|
||||
import { dockerConnectionCliFlags } from '../utils/docker';
|
||||
import { parseAsInteger } from '../utils/validation';
|
||||
import { isV13 } from '../utils/version';
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import * as _ from 'lodash';
|
||||
@ -40,7 +35,6 @@ import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk';
|
||||
import type { Preloader } from 'balena-preload';
|
||||
|
||||
interface FlagsDef extends DockerConnectionCliFlags {
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
commit?: string;
|
||||
'splash-image'?: string;
|
||||
@ -99,7 +93,6 @@ export default class PreloadCmd extends Command {
|
||||
public static usage = 'preload <image>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13() ? {} : { app: cf.application }),
|
||||
fleet: cf.fleet,
|
||||
commit: flags.string({
|
||||
description: `\
|
||||
@ -163,11 +156,6 @@ Can be repeated to add multiple certificates.\
|
||||
PreloadCmd,
|
||||
);
|
||||
|
||||
if (options.app && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.app ||= options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const balenaPreload = await import('balena-preload');
|
||||
const visuals = getVisuals();
|
||||
@ -194,15 +182,9 @@ Can be repeated to add multiple certificates.\
|
||||
|
||||
// balena-preload currently does not work with numerical app IDs
|
||||
// Load app here, and use app slug from hereon
|
||||
if (options.app && !options.app.includes('/')) {
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const { getApplication } = await import('../utils/sdk');
|
||||
const application = await getApplication(balena, options.app);
|
||||
if (!application) {
|
||||
throw new ExpectedError(`Fleet not found: ${options.app}`);
|
||||
}
|
||||
options.app = application.slug;
|
||||
}
|
||||
const fleetSlug: string | undefined = options.fleet
|
||||
? await (await import('../utils/sdk')).getFleetSlug(balena, options.fleet)
|
||||
: undefined;
|
||||
|
||||
const progressBars: {
|
||||
[key: string]: ReturnType<typeof getVisuals>['Progress'];
|
||||
@ -238,15 +220,12 @@ Can be repeated to add multiple certificates.\
|
||||
? 'latest'
|
||||
: options.commit;
|
||||
const image = params.image;
|
||||
const appId = options.app;
|
||||
|
||||
const splashImage = options['splash-image'];
|
||||
const additionalSpace = options['additional-space'];
|
||||
|
||||
const dontCheckArch = options['dont-check-arch'] || false;
|
||||
const pinDevice = options['pin-device-to-release'] || false;
|
||||
|
||||
if (dontCheckArch && !appId) {
|
||||
if (dontCheckArch && !fleetSlug) {
|
||||
throw new ExpectedError(
|
||||
'You need to specify a fleet if you disable the architecture check.',
|
||||
);
|
||||
@ -265,7 +244,7 @@ Can be repeated to add multiple certificates.\
|
||||
const preloader = new balenaPreload.Preloader(
|
||||
null,
|
||||
docker,
|
||||
appId,
|
||||
fleetSlug,
|
||||
commit,
|
||||
image,
|
||||
splashImage,
|
||||
@ -309,7 +288,7 @@ Can be repeated to add multiple certificates.\
|
||||
preloader.on('error', reject);
|
||||
resolve(
|
||||
this.prepareAndPreload(preloader, balena, {
|
||||
appId,
|
||||
appId: fleetSlug,
|
||||
commit,
|
||||
pinDevice,
|
||||
}),
|
||||
@ -364,8 +343,8 @@ Can be repeated to add multiple certificates.\
|
||||
} catch {
|
||||
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
|
||||
}
|
||||
return (await balena.models.application.getAll({
|
||||
$select: ['id', 'app_name', 'should_track_latest_release'],
|
||||
return (await balena.models.application.getAllDirectlyAccessible({
|
||||
$select: ['id', 'slug', 'should_track_latest_release'],
|
||||
$expand: this.applicationExpandOptions,
|
||||
$filter: {
|
||||
// get the apps that are of the same arch as the device type of the image
|
||||
@ -408,7 +387,7 @@ Can be repeated to add multiple certificates.\
|
||||
},
|
||||
},
|
||||
},
|
||||
$orderby: 'app_name asc',
|
||||
$orderby: 'slug asc',
|
||||
})) as Array<
|
||||
ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
@ -437,7 +416,7 @@ Can be repeated to add multiple certificates.\
|
||||
message: 'Select a fleet',
|
||||
type: 'list',
|
||||
choices: applications.map((app) => ({
|
||||
name: app.app_name,
|
||||
name: app.slug,
|
||||
value: app,
|
||||
})),
|
||||
});
|
||||
@ -512,7 +491,7 @@ Would you like to disable automatic updates for this fleet now?\
|
||||
});
|
||||
}
|
||||
|
||||
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) {
|
||||
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string) {
|
||||
const { getApplication } = await import('../utils/sdk');
|
||||
|
||||
return (await getApplication(balenaSdk, appId, {
|
||||
|
@ -22,7 +22,6 @@ import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import type { BalenaSDK } from 'balena-sdk';
|
||||
import { ExpectedError, instanceOf } from '../errors';
|
||||
import { isV13 } from '../utils/version';
|
||||
import { RegistrySecrets } from 'resin-multibuild';
|
||||
import { lowercaseIfSlug } from '../utils/normalization';
|
||||
import {
|
||||
@ -47,14 +46,11 @@ interface FlagsDef {
|
||||
pull: boolean;
|
||||
'noparent-check': boolean;
|
||||
'registry-secrets'?: string;
|
||||
gitignore?: boolean; // v13: delete this flag
|
||||
nogitignore?: boolean; // v13: delete this flag
|
||||
nolive: boolean;
|
||||
detached: boolean;
|
||||
service?: string[];
|
||||
system: boolean;
|
||||
env?: string[];
|
||||
'convert-eol'?: boolean;
|
||||
'noconvert-eol': boolean;
|
||||
'multi-dockerignore': boolean;
|
||||
'release-tag'?: string[];
|
||||
@ -142,6 +138,7 @@ export default class PushCmd extends Command {
|
||||
char: 'e',
|
||||
default: false,
|
||||
}),
|
||||
// TODO: docker-compose naming
|
||||
dockerfile: flags.string({
|
||||
description:
|
||||
'Alternative Dockerfile name/path, relative to the source folder',
|
||||
@ -218,16 +215,6 @@ export default class PushCmd extends Command {
|
||||
`,
|
||||
multiple: true,
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
'convert-eol': flags.boolean({
|
||||
description: 'No-op and deprecated since balena CLI v12.0.0',
|
||||
char: 'l',
|
||||
hidden: true,
|
||||
default: false,
|
||||
}),
|
||||
}),
|
||||
'noconvert-eol': flags.boolean({
|
||||
description: `Don't convert line endings from CRLF (Windows format) to LF (Unix format).`,
|
||||
default: false,
|
||||
@ -237,28 +224,7 @@ export default class PushCmd extends Command {
|
||||
'Have each service use its own .dockerignore file. See "balena help push".',
|
||||
char: 'm',
|
||||
default: false,
|
||||
exclusive: ['gitignore'], // v13: delete this line
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
gitignore: flags.boolean({
|
||||
description: stripIndent`
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is
|
||||
required until your project can be adapted.`,
|
||||
char: 'g',
|
||||
default: false,
|
||||
exclusive: ['multi-dockerignore'],
|
||||
}),
|
||||
nogitignore: flags.boolean({
|
||||
description:
|
||||
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
|
||||
char: 'G',
|
||||
hidden: true,
|
||||
default: false,
|
||||
}),
|
||||
}),
|
||||
'release-tag': flags.string({
|
||||
description: stripIndent`
|
||||
Set release tags if the image build is successful (balenaCloud only). Multiple
|
||||
@ -378,7 +344,6 @@ export default class PushCmd extends Command {
|
||||
source: options.source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
nogitignore: !options.gitignore, // v13: delete this line
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
@ -422,7 +387,6 @@ export default class PushCmd extends Command {
|
||||
multiDockerignore: options['multi-dockerignore'],
|
||||
nocache: options.nocache,
|
||||
pull: options.pull,
|
||||
nogitignore: !options.gitignore, // v13: delete this line
|
||||
noParentCheck: options['noparent-check'],
|
||||
nolive: options.nolive,
|
||||
detached: options.detached,
|
||||
|
@ -49,7 +49,7 @@ export default class ReleasesCmd extends Command {
|
||||
public static args = [
|
||||
{
|
||||
name: 'fleet',
|
||||
description: 'fleet name or slug',
|
||||
description: 'fleet name or slug (preferred)',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
@ -69,9 +69,10 @@ export default class ReleasesCmd extends Command {
|
||||
];
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const { getFleetSlug } = await import('../utils/sdk');
|
||||
|
||||
const releases = await balena.models.release.getAllByApplication(
|
||||
params.fleet,
|
||||
await getFleetSlug(balena, params.fleet),
|
||||
{ $select: fields },
|
||||
);
|
||||
|
||||
|
@ -20,7 +20,6 @@ import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { parseAsInteger, validateLocalHostnameOrIp } from '../utils/validation';
|
||||
import * as BalenaSdk from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
port?: number;
|
||||
@ -128,8 +127,8 @@ export default class SshCmd extends Command {
|
||||
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
|
||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||
return await performLocalDeviceSSH({
|
||||
address: params.fleetOrDevice,
|
||||
port: options.port,
|
||||
hostname: params.fleetOrDevice,
|
||||
port: options.port || 'local',
|
||||
forceTTY: options.tty,
|
||||
verbose: options.verbose,
|
||||
service: params.service,
|
||||
@ -152,12 +151,6 @@ export default class SshCmd extends Command {
|
||||
params.fleetOrDevice,
|
||||
);
|
||||
|
||||
const device = await sdk.models.device.get(deviceUuid, {
|
||||
$select: ['id', 'supervisor_version', 'is_online'],
|
||||
});
|
||||
|
||||
const deviceId = device.id;
|
||||
const supervisorVersion = device.supervisor_version;
|
||||
const { which } = await import('../utils/which');
|
||||
|
||||
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
|
||||
@ -209,19 +202,15 @@ export default class SshCmd extends Command {
|
||||
// that we know exists and is accessible
|
||||
let containerId: string | undefined;
|
||||
if (params.service != null) {
|
||||
containerId = await this.getContainerId(
|
||||
sdk,
|
||||
const { getContainerIdForService } = await import('../utils/device/ssh');
|
||||
containerId = await getContainerIdForService({
|
||||
deviceUuid,
|
||||
params.service,
|
||||
{
|
||||
port: options.port,
|
||||
proxyCommand,
|
||||
proxyUrl: proxyUrl || '',
|
||||
username: username!,
|
||||
},
|
||||
supervisorVersion,
|
||||
deviceId,
|
||||
);
|
||||
hostname: `ssh.${proxyUrl}`,
|
||||
port: options.port || 'cloud',
|
||||
proxyCommand,
|
||||
service: params.service,
|
||||
username: username!,
|
||||
});
|
||||
}
|
||||
|
||||
let accessCommand: string;
|
||||
@ -230,158 +219,14 @@ export default class SshCmd extends Command {
|
||||
} else {
|
||||
accessCommand = `host ${deviceUuid}`;
|
||||
}
|
||||
|
||||
const command = this.generateVpnSshCommand({
|
||||
uuid: deviceUuid,
|
||||
command: accessCommand,
|
||||
verbose: options.verbose,
|
||||
port: options.port,
|
||||
const { runRemoteCommand } = await import('../utils/ssh');
|
||||
await runRemoteCommand({
|
||||
cmd: accessCommand,
|
||||
hostname: `ssh.${proxyUrl}`,
|
||||
port: options.port || 'cloud',
|
||||
proxyCommand,
|
||||
proxyUrl: proxyUrl || '',
|
||||
username: username!,
|
||||
username,
|
||||
verbose: options.verbose,
|
||||
});
|
||||
|
||||
const { spawnSshAndThrowOnError } = await import('../utils/ssh');
|
||||
return spawnSshAndThrowOnError(command);
|
||||
}
|
||||
|
||||
async getContainerId(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
uuid: string,
|
||||
serviceName: string,
|
||||
sshOpts: {
|
||||
port?: number;
|
||||
proxyCommand?: string[];
|
||||
proxyUrl: string;
|
||||
username: string;
|
||||
},
|
||||
version?: string,
|
||||
id?: number,
|
||||
): Promise<string> {
|
||||
const semver = await import('balena-semver');
|
||||
|
||||
if (version == null || id == null) {
|
||||
const device = await sdk.models.device.get(uuid, {
|
||||
$select: ['id', 'supervisor_version'],
|
||||
});
|
||||
version = device.supervisor_version;
|
||||
id = device.id;
|
||||
}
|
||||
|
||||
let containerId: string | undefined;
|
||||
if (semver.gte(version, '8.6.0')) {
|
||||
const apiUrl = await sdk.settings.get('apiUrl');
|
||||
// TODO: Move this into the SDKs device model
|
||||
const request = await sdk.request.send({
|
||||
method: 'POST',
|
||||
url: '/supervisor/v2/containerId',
|
||||
baseUrl: apiUrl,
|
||||
body: {
|
||||
method: 'GET',
|
||||
deviceId: id,
|
||||
},
|
||||
});
|
||||
if (request.status !== 200) {
|
||||
throw new Error(
|
||||
`There was an error connecting to device ${uuid}, HTTP response code: ${request.status}.`,
|
||||
);
|
||||
}
|
||||
const body = request.body;
|
||||
if (body.status !== 'success') {
|
||||
throw new Error(
|
||||
`There was an error communicating with device ${uuid}.\n\tError: ${body.message}`,
|
||||
);
|
||||
}
|
||||
containerId = body.services[serviceName];
|
||||
} else {
|
||||
console.error(stripIndent`
|
||||
Using legacy method to detect container ID. This will be slow.
|
||||
To speed up this process, please update your device to an OS
|
||||
which has a supervisor version of at least v8.6.0.
|
||||
`);
|
||||
// We need to execute a balena ps command on the device,
|
||||
// and parse the output, looking for a specific
|
||||
// container
|
||||
const childProcess = await import('child_process');
|
||||
const { escapeRegExp } = await import('lodash');
|
||||
const { which } = await import('../utils/which');
|
||||
const { deviceContainerEngineBinary } = await import(
|
||||
'../utils/device/ssh'
|
||||
);
|
||||
|
||||
const sshBinary = await which('ssh');
|
||||
const sshArgs = this.generateVpnSshCommand({
|
||||
uuid,
|
||||
verbose: false,
|
||||
port: sshOpts.port,
|
||||
command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`,
|
||||
proxyCommand: sshOpts.proxyCommand,
|
||||
proxyUrl: sshOpts.proxyUrl,
|
||||
username: sshOpts.username,
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`);
|
||||
}
|
||||
const subProcess = childProcess.spawn(sshBinary, sshArgs, {
|
||||
stdio: [null, 'pipe', null],
|
||||
});
|
||||
const containers = await new Promise<string>((resolve, reject) => {
|
||||
const output: string[] = [];
|
||||
subProcess.stdout.on('data', (chunk) => output.push(chunk.toString()));
|
||||
subProcess.on('close', (code: number) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`Non-zero error code when looking for service container: ${code}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resolve(output.join(''));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const lines = containers.split('\n');
|
||||
const regex = new RegExp(`\\/?${escapeRegExp(serviceName)}_\\d+_\\d+`);
|
||||
for (const container of lines) {
|
||||
const [cId, name] = container.split(' ');
|
||||
if (regex.test(name)) {
|
||||
containerId = cId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (containerId == null) {
|
||||
throw new Error(
|
||||
`Could not find a service ${serviceName} on device ${uuid}.`,
|
||||
);
|
||||
}
|
||||
return containerId;
|
||||
}
|
||||
|
||||
generateVpnSshCommand(opts: {
|
||||
uuid: string;
|
||||
command: string;
|
||||
verbose: boolean;
|
||||
port?: number;
|
||||
username: string;
|
||||
proxyUrl: string;
|
||||
proxyCommand?: string[];
|
||||
}) {
|
||||
return [
|
||||
...(opts.verbose ? ['-vvv'] : []),
|
||||
'-t',
|
||||
...['-o', 'LogLevel=ERROR'],
|
||||
...['-o', 'StrictHostKeyChecking=no'],
|
||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||
...(opts.proxyCommand && opts.proxyCommand.length
|
||||
? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`]
|
||||
: []),
|
||||
...(opts.port ? ['-p', opts.port.toString()] : []),
|
||||
`${opts.username}@ssh.${opts.proxyUrl}`,
|
||||
opts.command,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -20,15 +20,9 @@ import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../utils/messages';
|
||||
import { isV13 } from '../utils/version';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
duration?: string;
|
||||
@ -77,11 +71,10 @@ export default class SupportCmd extends Command {
|
||||
description: 'comma-separated list (no spaces) of device UUIDs',
|
||||
char: 'd',
|
||||
}),
|
||||
...(isV13() ? {} : { application: cf.application }),
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
description:
|
||||
'comma-separated list (no spaces) of fleet names or org/name slugs',
|
||||
'comma-separated list (no spaces) of fleet names or slugs (preferred)',
|
||||
},
|
||||
duration: flags.string({
|
||||
description:
|
||||
@ -98,18 +91,13 @@ export default class SupportCmd extends Command {
|
||||
SupportCmd,
|
||||
);
|
||||
|
||||
if (options.application && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const enabling = params.action === 'enable';
|
||||
|
||||
// Validation
|
||||
if (!options.device && !options.application) {
|
||||
if (!options.device && !options.fleet) {
|
||||
throw new ExpectedError('At least one device or fleet must be specified');
|
||||
}
|
||||
|
||||
@ -125,7 +113,7 @@ export default class SupportCmd extends Command {
|
||||
const expiryTs = Date.now() + this.parseDuration(duration);
|
||||
|
||||
const deviceUuids = options.device?.split(',') || [];
|
||||
const appNames = options.application?.split(',') || [];
|
||||
const appNames = options.fleet?.split(',') || [];
|
||||
|
||||
const enablingMessage = 'Enabling support access for';
|
||||
const disablingMessage = 'Disabling support access for';
|
||||
@ -142,14 +130,17 @@ export default class SupportCmd extends Command {
|
||||
ux.action.stop();
|
||||
}
|
||||
|
||||
const { getFleetSlug } = await import('../utils/sdk');
|
||||
|
||||
// Process applications
|
||||
for (const appName of appNames) {
|
||||
const slug = await getFleetSlug(balena, appName);
|
||||
if (enabling) {
|
||||
ux.action.start(`${enablingMessage} fleet ${appName}`);
|
||||
await balena.models.application.grantSupportAccess(appName, expiryTs);
|
||||
ux.action.start(`${enablingMessage} fleet ${slug}`);
|
||||
await balena.models.application.grantSupportAccess(slug, expiryTs);
|
||||
} else if (params.action === 'disable') {
|
||||
ux.action.start(`${disablingMessage} fleet ${appName}`);
|
||||
await balena.models.application.revokeSupportAccess(appName);
|
||||
ux.action.start(`${disablingMessage} fleet ${slug}`);
|
||||
await balena.models.application.revokeSupportAccess(slug);
|
||||
}
|
||||
ux.action.stop();
|
||||
}
|
||||
|
@ -19,16 +19,9 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
app?: string;
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
@ -67,29 +60,17 @@ export default class TagRmCmd extends Command {
|
||||
public static usage = 'tag rm <tagKey>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device', 'release'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'fleet', 'device', 'release'],
|
||||
},
|
||||
}),
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['app', 'application', 'device', 'release'],
|
||||
exclusive: ['device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'fleet', 'release'],
|
||||
exclusive: ['fleet', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'fleet', 'device'],
|
||||
exclusive: ['fleet', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
@ -101,25 +82,20 @@ export default class TagRmCmd extends Command {
|
||||
TagRmCmd,
|
||||
);
|
||||
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Check user has specified one of application/device/release
|
||||
if (!options.application && !options.device && !options.release) {
|
||||
if (!options.fleet && !options.device && !options.release) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(TagRmCmd.missingResourceMessage);
|
||||
}
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
|
||||
if (options.application) {
|
||||
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
|
||||
if (options.fleet) {
|
||||
const { getFleetSlug } = await import('../../utils/sdk');
|
||||
return balena.models.application.tags.remove(
|
||||
await getTypedApplicationIdentifier(balena, options.application),
|
||||
await getFleetSlug(balena, options.fleet),
|
||||
params.tagKey,
|
||||
);
|
||||
}
|
||||
|
@ -19,16 +19,9 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
app?: string;
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
@ -80,29 +73,17 @@ export default class TagSetCmd extends Command {
|
||||
public static usage = 'tag set <tagKey> [value]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device', 'release'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'fleet', 'device', 'release'],
|
||||
},
|
||||
}),
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['app', 'application', 'device', 'release'],
|
||||
exclusive: ['device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'fleet', 'release'],
|
||||
exclusive: ['fleet', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'fleet', 'device'],
|
||||
exclusive: ['fleet', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
@ -114,15 +95,10 @@ export default class TagSetCmd extends Command {
|
||||
TagSetCmd,
|
||||
);
|
||||
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Check user has specified one of application/device/release
|
||||
if (!options.application && !options.device && !options.release) {
|
||||
if (!options.fleet && !options.device && !options.release) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(TagSetCmd.missingResourceMessage);
|
||||
}
|
||||
@ -131,10 +107,10 @@ export default class TagSetCmd extends Command {
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
|
||||
if (options.application) {
|
||||
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
|
||||
if (options.fleet) {
|
||||
const { getFleetSlug } = await import('../../utils/sdk');
|
||||
return balena.models.application.tags.set(
|
||||
await getTypedApplicationIdentifier(balena, options.application),
|
||||
await getFleetSlug(balena, options.fleet),
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
|
@ -20,16 +20,9 @@ import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../utils/messages';
|
||||
import { isV13 } from '../utils/version';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
app?: string;
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
@ -56,29 +49,17 @@ export default class TagsCmd extends Command {
|
||||
public static usage = 'tags';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device', 'release'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'fleet', 'device', 'release'],
|
||||
},
|
||||
}),
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['app', 'application', 'device', 'release'],
|
||||
exclusive: ['device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'fleet', 'release'],
|
||||
exclusive: ['fleet', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'fleet', 'device'],
|
||||
exclusive: ['fleet', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
@ -88,15 +69,10 @@ export default class TagsCmd extends Command {
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
|
||||
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Check user has specified one of application/device/release
|
||||
if (!options.application && !options.device && !options.release) {
|
||||
if (!options.fleet && !options.device && !options.release) {
|
||||
throw new ExpectedError(this.missingResourceMessage);
|
||||
}
|
||||
|
||||
@ -104,10 +80,10 @@ export default class TagsCmd extends Command {
|
||||
|
||||
let tags;
|
||||
|
||||
if (options.application) {
|
||||
const { getTypedApplicationIdentifier } = await import('../utils/sdk');
|
||||
if (options.fleet) {
|
||||
const { getFleetSlug } = await import('../utils/sdk');
|
||||
tags = await balena.models.application.tags.getAllByApplication(
|
||||
await getTypedApplicationIdentifier(balena, options.application),
|
||||
await getFleetSlug(balena, options.fleet),
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
|
@ -136,8 +136,7 @@ export default class TunnelCmd extends Command {
|
||||
// Ascertain device uuid
|
||||
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
|
||||
const uuid = await getOnlineTargetDeviceUuid(sdk, params.deviceOrFleet);
|
||||
const device = await sdk.models.device.get(uuid);
|
||||
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
|
||||
logger.logInfo(`Opening a tunnel to ${uuid}...`);
|
||||
|
||||
const _ = await import('lodash');
|
||||
const localListeners = _.chain(options.port)
|
||||
@ -147,11 +146,7 @@ export default class TunnelCmd extends Command {
|
||||
.map(async ({ localPort, localAddress, remotePort }) => {
|
||||
try {
|
||||
const { tunnelConnectionToDevice } = await import('../utils/tunnel');
|
||||
const handler = await tunnelConnectionToDevice(
|
||||
device.uuid,
|
||||
remotePort,
|
||||
sdk,
|
||||
);
|
||||
const handler = await tunnelConnectionToDevice(uuid, remotePort, sdk);
|
||||
|
||||
const { createServer } = await import('net');
|
||||
const server = createServer(async (client: Socket) => {
|
||||
@ -162,7 +157,7 @@ export default class TunnelCmd extends Command {
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
uuid,
|
||||
remotePort,
|
||||
);
|
||||
} catch (err) {
|
||||
@ -171,7 +166,7 @@ export default class TunnelCmd extends Command {
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
uuid,
|
||||
remotePort,
|
||||
err,
|
||||
);
|
||||
@ -186,15 +181,15 @@ export default class TunnelCmd extends Command {
|
||||
});
|
||||
|
||||
logger.logInfo(
|
||||
` - tunnelling ${localAddress}:${localPort} to ${device.uuid}:${remotePort}`,
|
||||
` - tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.logWarn(
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}, failed ${JSON.stringify(
|
||||
err.message,
|
||||
)}`,
|
||||
);
|
||||
|
||||
return false;
|
||||
|
@ -16,12 +16,7 @@
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { getBalenaSdk, stripIndent } from './utils/lazy';
|
||||
|
||||
interface CachedUsername {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
import { stripIndent } from './utils/lazy';
|
||||
|
||||
/**
|
||||
* Track balena CLI usage events (product improvement analytics).
|
||||
@ -49,40 +44,13 @@ export async function trackCommand(commandSignature: string) {
|
||||
scope.setExtra('command', commandSignature);
|
||||
});
|
||||
}
|
||||
const settings = await import('balena-settings-client');
|
||||
|
||||
const username = await (async () => {
|
||||
const getStorage = await import('balena-settings-storage');
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
const storage = getStorage({ dataDirectory });
|
||||
let token;
|
||||
try {
|
||||
token = await storage.get('token');
|
||||
} catch {
|
||||
// If we can't get a token then we can't get a username
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = (await storage.get('cachedUsername')) as CachedUsername;
|
||||
if (result.token === token) {
|
||||
return result.username;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const balena = getBalenaSdk();
|
||||
const $username = await balena.auth.whoami();
|
||||
await storage.set('cachedUsername', {
|
||||
token,
|
||||
username: $username,
|
||||
} as CachedUsername);
|
||||
return $username;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
const { getCachedUsername } = await import('./utils/bootstrap');
|
||||
let username: string | undefined;
|
||||
try {
|
||||
username = (await getCachedUsername())?.username;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry!.configureScope((scope) => {
|
||||
scope.setUser({
|
||||
@ -96,6 +64,7 @@ export async function trackCommand(commandSignature: string) {
|
||||
!process.env.BALENA_CLI_TEST_TYPE &&
|
||||
!process.env.BALENARC_NO_ANALYTICS
|
||||
) {
|
||||
const settings = await import('balena-settings-client');
|
||||
const balenaUrl = settings.get<string>('balenaUrl');
|
||||
await sendEvent(balenaUrl, `[CLI] ${commandSignature}`, username);
|
||||
}
|
||||
|
@ -27,14 +27,7 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
// `@types/node` does not know about `options: { bigint?: boolean }`
|
||||
type statT = (
|
||||
fPath: string,
|
||||
options: { bigint?: boolean },
|
||||
) => fs.Stats | Promise<fs.Stats>;
|
||||
|
||||
// async stat does not work with pkg's internal `/snapshot` filesystem
|
||||
const stat: statT = process.pkg ? fs.statSync : fs.promises.stat;
|
||||
const stat = process.pkg ? fs.statSync : fs.promises.stat;
|
||||
|
||||
let fastBootStarted = false;
|
||||
|
||||
|
19
lib/framework/index.ts
Normal file
19
lib/framework/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import type { DataOutputOptions, DataSetOutputOptions } from './output';
|
||||
|
||||
export { DataOutputOptions, DataSetOutputOptions };
|
158
lib/framework/output.ts
Normal file
158
lib/framework/output.ts
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { getCliUx, getChalk } from '../utils/lazy';
|
||||
|
||||
export interface DataOutputOptions {
|
||||
fields?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export interface DataSetOutputOptions extends DataOutputOptions {
|
||||
filter?: string;
|
||||
'no-header'?: boolean;
|
||||
'no-truncate'?: boolean;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output message to STDERR
|
||||
*/
|
||||
export function outputMessage(msg: string) {
|
||||
// Messages go to STDERR
|
||||
console.error(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output result data to STDOUT
|
||||
* Supports:
|
||||
* - arrays of items (displayed in a tabular way),
|
||||
* - single items (displayed in a field per row format).
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
export async function outputData(
|
||||
data: any[] | {},
|
||||
fields: string[],
|
||||
options: DataOutputOptions | DataSetOutputOptions,
|
||||
) {
|
||||
if (Array.isArray(data)) {
|
||||
await outputDataSet(data, fields, options as DataSetOutputOptions);
|
||||
} else {
|
||||
await outputDataItem(data, fields, options as DataOutputOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the cli.ux table implementation, to output tabular data
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
async function outputDataSet(
|
||||
data: any[],
|
||||
fields: string[],
|
||||
options: DataSetOutputOptions,
|
||||
) {
|
||||
// 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
|
||||
// (e.g. as seen in json output).
|
||||
options.fields = options.fields?.replace(/_/g, ' ');
|
||||
options.filter = options.filter?.replace(/_/g, ' ');
|
||||
options.sort = options.sort?.replace(/_/g, ' ');
|
||||
|
||||
getCliUx().table(
|
||||
data,
|
||||
// Convert fields array to column object keys
|
||||
// that cli.ux expects. We can later add support
|
||||
// for both formats if beneficial
|
||||
fields.reduce((ac, a) => ({ ...ac, [a]: {} }), {}),
|
||||
{
|
||||
...options,
|
||||
...(options.json
|
||||
? {
|
||||
output: 'json',
|
||||
}
|
||||
: {}),
|
||||
columns: options.fields,
|
||||
printLine,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a single data object (like `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
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
async function outputDataItem(
|
||||
data: any,
|
||||
fields: string[],
|
||||
options: DataOutputOptions,
|
||||
) {
|
||||
const 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`),
|
||||
// or the format displayed in headers (e.g. `Some field`, case insensitive).
|
||||
const userSelectedFields = options.fields?.split(',').map((f) => {
|
||||
return f.toLowerCase().trim().replace(/ /g, '_');
|
||||
});
|
||||
|
||||
// Order and filter the fields based on `fields` parameter and `options.fields`
|
||||
(userSelectedFields || fields).forEach((fieldName) => {
|
||||
if (fields.includes(fieldName)) {
|
||||
outData[fieldName] = data[fieldName];
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// Output one field per line
|
||||
for (const [k, v] of Object.entries(outData)) {
|
||||
const shim = ' '.repeat(longestKeyLength - k.length);
|
||||
const kDisplay = capitalize(k.replace(/_/g, ' '));
|
||||
printLine(`${chalk.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;
|
||||
}
|
||||
|
||||
function printLine(s: any) {
|
||||
// Duplicating oclif cli-ux's default implementation here,
|
||||
// but using this one explicitly for ease of testing
|
||||
process.stdout.write(s + '\n');
|
||||
}
|
@ -131,6 +131,13 @@ Please use "balena ${alternative}" instead.`);
|
||||
'local scan': [replaced, 'scan', 'v11.0.0'],
|
||||
'local ssh': [replaced, 'ssh', 'v11.0.0'],
|
||||
'local stop': [removed, stopAlternative, 'v11.0.0'],
|
||||
app: [replaced, 'fleet', 'v13.0.0'],
|
||||
apps: [replaced, 'fleets', 'v13.0.0'],
|
||||
'app create': [replaced, 'fleet create', 'v13.0.0'],
|
||||
'app purge': [replaced, 'fleet purge', 'v13.0.0'],
|
||||
'app rename': [replaced, 'fleet rename', 'v13.0.0'],
|
||||
'app restart': [replaced, 'fleet restart', 'v13.0.0'],
|
||||
'app rm': [replaced, 'fleet rm', 'v13.0.0'],
|
||||
};
|
||||
let cmd: string | undefined;
|
||||
if (argvSlice.length > 1) {
|
||||
|
@ -119,3 +119,61 @@ export async function pkgExec(modFunc: string, args: string[]) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CachedUsername {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
let cachedUsername: CachedUsername | undefined;
|
||||
|
||||
/**
|
||||
* Return the parsed contents of the `~/.balena/cachedUsername` file. If the file
|
||||
* does not exist, create it with the details from the cloud. If not connected
|
||||
* to the internet, return undefined. This function is used by `lib/events.ts`
|
||||
* (event tracking) and `lib/utils/device/ssh.ts` and needs to gracefully handle
|
||||
* the scenario of not being connected to the internet.
|
||||
*/
|
||||
export async function getCachedUsername(): Promise<CachedUsername | undefined> {
|
||||
if (cachedUsername) {
|
||||
return cachedUsername;
|
||||
}
|
||||
const [{ getBalenaSdk }, getStorage, settings] = await Promise.all([
|
||||
import('./lazy'),
|
||||
import('balena-settings-storage'),
|
||||
import('balena-settings-client'),
|
||||
]);
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
const storage = getStorage({ dataDirectory });
|
||||
let token: string | undefined;
|
||||
try {
|
||||
token = (await storage.get('token')) as string | undefined;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (!token) {
|
||||
// If we can't get a token then we can't get a username
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = (await storage.get('cachedUsername')) as
|
||||
| CachedUsername
|
||||
| undefined;
|
||||
if (result && result.token === token && result.username) {
|
||||
cachedUsername = result;
|
||||
return cachedUsername;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const username = await getBalenaSdk().auth.whoami();
|
||||
if (username) {
|
||||
cachedUsername = { token, username };
|
||||
await storage.set('cachedUsername', cachedUsername);
|
||||
}
|
||||
} catch {
|
||||
// ignore (not connected to the internet?)
|
||||
}
|
||||
return cachedUsername;
|
||||
}
|
||||
|
@ -107,6 +107,16 @@ export const getDeviceAndMaybeAppFromUUID = _.memoize(
|
||||
(_sdk, deviceUUID) => deviceUUID,
|
||||
);
|
||||
|
||||
/** Given a device type alias like 'nuc', return the actual slug like 'intel-nuc'. */
|
||||
export const unaliasDeviceType = _.memoize(async function (
|
||||
sdk: SDK.BalenaSDK,
|
||||
deviceType: string,
|
||||
): Promise<string> {
|
||||
return (
|
||||
(await sdk.models.device.getManifestBySlug(deviceType)).slug || deviceType
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Download balenaOS image for the specified `deviceType`.
|
||||
* `OSVersion` may be one of:
|
||||
@ -199,56 +209,72 @@ async function resolveOSVersion(
|
||||
deviceType: string,
|
||||
version: string,
|
||||
): Promise<string> {
|
||||
if (!['menu', 'menu-esr'].includes(version)) {
|
||||
if (['menu', 'menu-esr'].includes(version)) {
|
||||
return await selectOSVersionFromMenu(deviceType, version === 'menu-esr');
|
||||
}
|
||||
// Note that `version` may also be 'latest', 'recommended', 'default'
|
||||
if (/^v?\d+\.\d+\.\d+/.test(version)) {
|
||||
if (version[0] === 'v') {
|
||||
version = version.slice(1);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
const vs = await getFormattedOsVersions(deviceType, version === 'menu-esr');
|
||||
async function selectOSVersionFromMenu(
|
||||
deviceType: string,
|
||||
esr: boolean,
|
||||
): Promise<string> {
|
||||
const vs = await getOsVersions(deviceType, esr);
|
||||
|
||||
const choices = vs.map((v) => ({
|
||||
value: v.rawVersion,
|
||||
name: v.formattedVersion,
|
||||
value: v.raw_version,
|
||||
name: formatOsVersion(v),
|
||||
}));
|
||||
|
||||
return getCliForm().ask({
|
||||
message: 'Select the OS version:',
|
||||
type: 'list',
|
||||
choices,
|
||||
default: (vs.find((v) => v.isRecommended) ?? vs[0])?.rawVersion,
|
||||
default: vs[0]?.raw_version,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the output of sdk.models.hostapp.getAvailableOsVersions(), filtered
|
||||
* regarding ESR or non-ESR versions, and having the `formattedVersion` field
|
||||
* reformatted for compatibility with the pre-existing output format of the
|
||||
* `os versions` and `os download` commands.
|
||||
* Return the output of sdk.models.os.getAvailableOsVersions(), resolving
|
||||
* device type aliases and filtering with regard to ESR versions.
|
||||
*/
|
||||
export async function getFormattedOsVersions(
|
||||
export async function getOsVersions(
|
||||
deviceType: string,
|
||||
esr: boolean,
|
||||
): Promise<SDK.OsVersion[]> {
|
||||
const versions: SDK.OsVersion[] = (
|
||||
(await getBalenaSdk().models.hostapp.getAvailableOsVersions([deviceType]))[
|
||||
deviceType
|
||||
] ?? []
|
||||
)
|
||||
.filter((v: SDK.OsVersion) => v.osType === (esr ? 'esr' : 'default'))
|
||||
.map((v: SDK.OsVersion) => {
|
||||
const i = v.formattedVersion.indexOf(' ');
|
||||
v.formattedVersion =
|
||||
i < 0
|
||||
? `v${v.rawVersion}`
|
||||
: `v${v.rawVersion}${v.formattedVersion.substring(i)}`;
|
||||
return v;
|
||||
});
|
||||
const sdk = getBalenaSdk();
|
||||
let slug = deviceType;
|
||||
let versions: SDK.OsVersion[] = await sdk.models.os.getAvailableOsVersions(
|
||||
slug,
|
||||
);
|
||||
// if slug is an alias, fetch the real slug
|
||||
if (!versions.length) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Error: No balenaOS versions found for device type '${deviceType}'.
|
||||
Double-check the device type slug with 'balena devices supported'.`);
|
||||
// unaliasDeviceType() produces a nice error msg if slug is invalid
|
||||
slug = await unaliasDeviceType(sdk, slug);
|
||||
if (slug !== deviceType) {
|
||||
versions = await sdk.models.os.getAvailableOsVersions(slug);
|
||||
}
|
||||
}
|
||||
versions = versions.filter(
|
||||
(v: SDK.OsVersion) => v.osType === (esr ? 'esr' : 'default'),
|
||||
);
|
||||
if (!versions.length) {
|
||||
const vType = esr ? 'ESR versions' : 'versions';
|
||||
throw new ExpectedError(
|
||||
`Error: No balenaOS ${vType} found for device type '${deviceType}'.`,
|
||||
);
|
||||
}
|
||||
return versions;
|
||||
}
|
||||
|
||||
export function formatOsVersion(osVersion: SDK.OsVersion): string {
|
||||
return osVersion.line
|
||||
? `v${osVersion.raw_version} (${osVersion.line})`
|
||||
: `v${osVersion.raw_version}`;
|
||||
}
|
||||
|
@ -16,37 +16,16 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
||||
import { stripIndent } from './lazy';
|
||||
import { lowercaseIfSlug } from './normalization';
|
||||
import { isV13 } from './version';
|
||||
|
||||
export const v13: IBooleanFlag<boolean> = flags.boolean({
|
||||
description: stripIndent`\
|
||||
enable selected balena CLI v13 pre-release features, like the renaming
|
||||
from "application" to "fleet" in command output`,
|
||||
default: false,
|
||||
});
|
||||
import { isV14 } from './version';
|
||||
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
||||
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
|
||||
|
||||
export const application = flags.string({
|
||||
char: 'a',
|
||||
description: 'DEPRECATED alias for -f, --fleet',
|
||||
parse: lowercaseIfSlug,
|
||||
});
|
||||
// TODO: Consider remove second alias 'app' when we can, to simplify.
|
||||
export const app = flags.string({
|
||||
description: 'DEPRECATED alias for -f, --fleet',
|
||||
parse: lowercaseIfSlug,
|
||||
});
|
||||
export const fleet = flags.string({
|
||||
char: 'f',
|
||||
description: isV13()
|
||||
? 'fleet name, slug (preferred), or numeric ID (deprecated)'
|
||||
: // avoid the '(deprecated)' remark in v12 while cf.application and
|
||||
// cf.app are also described as deprecated, to avoid the impression
|
||||
// that cf.fleet is deprecated as well.
|
||||
'fleet name, slug (preferred), or numeric ID',
|
||||
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
|
||||
parse: lowercaseIfSlug,
|
||||
});
|
||||
|
||||
@ -91,6 +70,11 @@ export const force: IBooleanFlag<boolean> = flags.boolean({
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const dev: IBooleanFlag<boolean> = flags.boolean({
|
||||
description: 'Configure balenaOS to operate in development mode',
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const drive = flags.string({
|
||||
char: 'd',
|
||||
description: stripIndent`
|
||||
@ -113,15 +97,55 @@ export const deviceType = flags.string({
|
||||
required: true,
|
||||
});
|
||||
|
||||
export const deviceTypeIgnored = flags.string({
|
||||
description: 'ignored - no longer required',
|
||||
char: 't',
|
||||
required: false,
|
||||
hidden: isV13(),
|
||||
});
|
||||
export const deviceTypeIgnored = {
|
||||
...(isV14()
|
||||
? {}
|
||||
: {
|
||||
type: flags.string({
|
||||
description: 'ignored - no longer required',
|
||||
char: 't',
|
||||
required: false,
|
||||
hidden: true,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export const json: IBooleanFlag<boolean> = flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const dataOutputFlags: flags.Input<DataOutputOptions> = {
|
||||
fields: flags.string({
|
||||
description: 'only show provided fields (comma-separated)',
|
||||
}),
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
exclusive: ['no-truncate'],
|
||||
description: 'output in json format',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
export const dataSetOutputFlags: flags.Input<DataOutputOptions> &
|
||||
flags.Input<DataSetOutputOptions> = {
|
||||
...dataOutputFlags,
|
||||
filter: flags.string({
|
||||
description:
|
||||
'filter results by substring matching of a given field, eg: --filter field=foo',
|
||||
}),
|
||||
'no-header': flags.boolean({
|
||||
exclusive: ['json'],
|
||||
description: 'hide table header from output',
|
||||
default: false,
|
||||
}),
|
||||
'no-truncate': flags.boolean({
|
||||
exclusive: ['json'],
|
||||
description: 'do not truncate output to fit screen',
|
||||
default: false,
|
||||
}),
|
||||
sort: flags.string({
|
||||
description: `field to sort by (prepend '-' for descending order)`,
|
||||
}),
|
||||
};
|
||||
|
15
lib/utils/compose-types.d.ts
vendored
15
lib/utils/compose-types.d.ts
vendored
@ -15,6 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ImageModel, ReleaseModel } from 'balena-release/build/models';
|
||||
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
|
||||
import type { Pack } from 'tar-stream';
|
||||
|
||||
@ -51,9 +52,8 @@ export interface ComposeOpts {
|
||||
dockerfilePath?: string;
|
||||
inlineLogs?: boolean;
|
||||
multiDockerignore: boolean;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
noParentCheck: boolean;
|
||||
projectName: string;
|
||||
projectName?: string;
|
||||
projectPath: string;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
@ -61,14 +61,12 @@ export interface ComposeOpts {
|
||||
export interface ComposeCliFlags {
|
||||
emulated: boolean;
|
||||
dockerfile?: string;
|
||||
dockercompose?: string;
|
||||
logs: boolean;
|
||||
nologs: boolean;
|
||||
gitignore?: boolean; // v13: delete this line
|
||||
nogitignore?: boolean; // v13: delete this line
|
||||
'multi-dockerignore': boolean;
|
||||
'noparent-check': boolean;
|
||||
'registry-secrets'?: RegistrySecrets;
|
||||
'convert-eol': boolean;
|
||||
'noconvert-eol': boolean;
|
||||
projectName?: string;
|
||||
}
|
||||
@ -83,7 +81,7 @@ export interface ComposeProject {
|
||||
export interface Release {
|
||||
client: ReturnType<typeof import('balena-release').createClient>;
|
||||
release: Pick<
|
||||
import('balena-release/build/models').ReleaseModel,
|
||||
ReleaseModel,
|
||||
| 'id'
|
||||
| 'status'
|
||||
| 'commit'
|
||||
@ -95,13 +93,14 @@ export interface Release {
|
||||
| 'start_timestamp'
|
||||
| 'end_timestamp'
|
||||
>;
|
||||
serviceImages: Partial<import('balena-release/build/models').ImageModel>;
|
||||
serviceImages: Dictionary<
|
||||
Omit<ImageModel, 'created_at' | 'is_a_build_of__service' | '__metadata'>
|
||||
>;
|
||||
}
|
||||
|
||||
interface TarDirectoryOptions {
|
||||
composition?: Composition;
|
||||
convertEol?: boolean;
|
||||
multiDockerignore?: boolean;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
|
||||
}
|
||||
|
@ -15,22 +15,32 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Renderer } from './compose_ts';
|
||||
import type * as SDK from 'balena-sdk';
|
||||
import type Dockerode = require('dockerode');
|
||||
import * as path from 'path';
|
||||
import { ExpectedError } from '../errors';
|
||||
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
|
||||
import type {
|
||||
BuiltImage,
|
||||
ComposeOpts,
|
||||
ComposeProject,
|
||||
Release,
|
||||
TaggedImage,
|
||||
} from './compose-types';
|
||||
import { getChalk } from './lazy';
|
||||
import { isV13 } from './version';
|
||||
import Logger = require('./logger');
|
||||
import { ProgressCallback } from 'docker-progress';
|
||||
|
||||
/**
|
||||
* @returns Promise<{import('./compose-types').ComposeOpts}>
|
||||
*/
|
||||
export function generateOpts(options) {
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
if (!isV13() && options.gitignore && options['multi-dockerignore']) {
|
||||
throw new ExpectedError(
|
||||
'The --gitignore and --multi-dockerignore options cannot be used together',
|
||||
);
|
||||
}
|
||||
export function generateOpts(options: {
|
||||
source?: string;
|
||||
projectName?: string;
|
||||
nologs: boolean;
|
||||
'noconvert-eol': boolean;
|
||||
dockerfile?: string;
|
||||
'multi-dockerignore': boolean;
|
||||
'noparent-check': boolean;
|
||||
}): Promise<ComposeOpts> {
|
||||
const { promises: fs } = require('fs') as typeof import('fs');
|
||||
return fs.realpath(options.source || '.').then((projectPath) => ({
|
||||
projectName: options.projectName,
|
||||
projectPath,
|
||||
@ -38,29 +48,23 @@ export function generateOpts(options) {
|
||||
convertEol: !options['noconvert-eol'],
|
||||
dockerfilePath: options.dockerfile,
|
||||
multiDockerignore: !!options['multi-dockerignore'],
|
||||
nogitignore: !options.gitignore, // v13: delete this line
|
||||
noParentCheck: options['noparent-check'],
|
||||
}));
|
||||
}
|
||||
|
||||
// Parse the given composition and return a structure with info. Input is:
|
||||
// - composePath: the *absolute* path to the directory containing the compose file
|
||||
// - composeStr: the contents of the compose file, as a string
|
||||
/**
|
||||
* @param {string} composePath
|
||||
* @param {string} composeStr
|
||||
* @param {string | undefined} projectName The --projectName flag (build, deploy)
|
||||
* @param {string | undefined} imageTag The --tag flag (build, deploy)
|
||||
* @returns {import('./compose-types').ComposeProject}
|
||||
/** Parse the given composition and return a structure with info. Input is:
|
||||
* - composePath: the *absolute* path to the directory containing the compose file
|
||||
* - composeStr: the contents of the compose file, as a string
|
||||
*/
|
||||
export function createProject(
|
||||
composePath,
|
||||
composeStr,
|
||||
composePath: string,
|
||||
composeStr: string,
|
||||
projectName = '',
|
||||
imageTag = '',
|
||||
) {
|
||||
const yml = require('js-yaml');
|
||||
const compose = require('resin-compose-parse');
|
||||
): ComposeProject {
|
||||
const yml = require('js-yaml') as typeof import('js-yaml');
|
||||
const compose =
|
||||
require('resin-compose-parse') as typeof import('resin-compose-parse');
|
||||
|
||||
// both methods below may throw.
|
||||
const rawComposition = yml.load(composeStr);
|
||||
@ -76,7 +80,8 @@ export function createProject(
|
||||
descr.image.context != null &&
|
||||
descr.image.tag == null
|
||||
) {
|
||||
const { makeImageName } = require('./compose_ts');
|
||||
const { makeImageName } =
|
||||
require('./compose_ts') as typeof import('./compose_ts');
|
||||
descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag);
|
||||
}
|
||||
return descr;
|
||||
@ -89,116 +94,20 @@ export function createProject(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the CLI v10 / v11 "original" tarDirectory function. It is still
|
||||
* around for the benefit of the `--gitignore` option, but is expected to be
|
||||
* deleted in CLI v13.
|
||||
* @param {string} dir Source directory
|
||||
* @param {import('./compose-types').TarDirectoryOptions} param
|
||||
* @returns {Promise<import('stream').Readable>}
|
||||
*
|
||||
* v13: delete this function
|
||||
*/
|
||||
export async function originalTarDirectory(dir, param) {
|
||||
let {
|
||||
preFinalizeCallback = null,
|
||||
convertEol = false,
|
||||
nogitignore = false,
|
||||
} = param;
|
||||
if (convertEol == null) {
|
||||
convertEol = false;
|
||||
}
|
||||
|
||||
const Bluebird = require('bluebird');
|
||||
const tar = require('tar-stream');
|
||||
const klaw = require('klaw');
|
||||
const { promises: fs } = require('fs');
|
||||
const streamToPromise = require('stream-to-promise');
|
||||
const { printGitignoreWarn } = require('./compose_ts');
|
||||
const { FileIgnorer, IgnoreFileType } = require('./ignore');
|
||||
const { toPosixPath } = require('resin-multibuild').PathUtils;
|
||||
let readFile;
|
||||
if (process.platform === 'win32') {
|
||||
const { readFileWithEolConversion } = require('./eol-conversion');
|
||||
readFile = (file) => readFileWithEolConversion(file, convertEol);
|
||||
} else {
|
||||
({ readFile } = fs);
|
||||
}
|
||||
|
||||
const getFiles = () =>
|
||||
Bluebird.resolve(streamToPromise(klaw(dir)))
|
||||
// @ts-ignore
|
||||
.filter((item) => !item.stats.isDirectory())
|
||||
// @ts-ignore
|
||||
.map((item) => item.path);
|
||||
|
||||
const ignore = new FileIgnorer(dir);
|
||||
const pack = tar.pack();
|
||||
const ignoreFiles = {};
|
||||
return getFiles()
|
||||
.each(function (file) {
|
||||
const type = ignore.getIgnoreFileType(path.relative(dir, file));
|
||||
if (type != null) {
|
||||
ignoreFiles[type] = ignoreFiles[type] || [];
|
||||
ignoreFiles[type].push(path.resolve(dir, file));
|
||||
return ignore.addIgnoreFile(file, type);
|
||||
}
|
||||
})
|
||||
.tap(() => {
|
||||
if (!nogitignore) {
|
||||
printGitignoreWarn(
|
||||
(ignoreFiles[IgnoreFileType.DockerIgnore] || [])[0] || '',
|
||||
ignoreFiles[IgnoreFileType.GitIgnore] || [],
|
||||
);
|
||||
}
|
||||
})
|
||||
.filter(ignore.filter)
|
||||
.map(function (file) {
|
||||
const relPath = path.relative(path.resolve(dir), file);
|
||||
return Promise.all([relPath, fs.stat(file), readFile(file)]).then(
|
||||
([filename, stats, data]) =>
|
||||
pack.entry(
|
||||
{
|
||||
name: toPosixPath(filename),
|
||||
mtime: stats.mtime,
|
||||
size: stats.size,
|
||||
mode: stats.mode,
|
||||
},
|
||||
data,
|
||||
),
|
||||
);
|
||||
})
|
||||
.then(() => preFinalizeCallback?.(pack))
|
||||
.then(function () {
|
||||
pack.finalize();
|
||||
return pack;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} apiEndpoint
|
||||
* @param {string} auth
|
||||
* @param {number} userId
|
||||
* @param {number} appId
|
||||
* @param {import('resin-compose-parse').Composition} composition
|
||||
* @param {boolean} draft
|
||||
* @param {string|undefined} semver
|
||||
* @param {string|undefined} contract
|
||||
* @returns {Promise<import('./compose-types').Release>}
|
||||
*/
|
||||
export const createRelease = async function (
|
||||
apiEndpoint,
|
||||
auth,
|
||||
userId,
|
||||
appId,
|
||||
composition,
|
||||
draft,
|
||||
semver,
|
||||
contract,
|
||||
) {
|
||||
const _ = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const releaseMod = require('balena-release');
|
||||
apiEndpoint: string,
|
||||
auth: string,
|
||||
userId: number,
|
||||
appId: number,
|
||||
composition: Composition,
|
||||
draft: boolean,
|
||||
semver?: string,
|
||||
contract?: string,
|
||||
): Promise<Release> {
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
const crypto = require('crypto') as typeof import('crypto');
|
||||
const releaseMod =
|
||||
require('balena-release') as typeof import('balena-release');
|
||||
|
||||
const client = releaseMod.createClient({ apiEndpoint, auth });
|
||||
|
||||
@ -228,24 +137,26 @@ export const createRelease = async function (
|
||||
'start_timestamp',
|
||||
'end_timestamp',
|
||||
]),
|
||||
serviceImages: _.mapValues(serviceImages, (serviceImage) =>
|
||||
_.omit(serviceImage, [
|
||||
'created_at',
|
||||
'is_a_build_of__service',
|
||||
'__metadata',
|
||||
]),
|
||||
serviceImages: _.mapValues(
|
||||
serviceImages,
|
||||
(serviceImage) =>
|
||||
_.omit(serviceImage, [
|
||||
'created_at',
|
||||
'is_a_build_of__service',
|
||||
'__metadata',
|
||||
]) as Omit<
|
||||
typeof serviceImage,
|
||||
'created_at' | 'is_a_build_of__service' | '__metadata'
|
||||
>,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('dockerode')} docker
|
||||
* @param {Array<import('./compose-types').BuiltImage>} images
|
||||
* @param {Partial<import('balena-release/build/models').ImageModel>} serviceImages
|
||||
* @returns {Promise<Array<import('./compose-types').TaggedImage>>}
|
||||
*/
|
||||
export const tagServiceImages = (docker, images, serviceImages) =>
|
||||
export const tagServiceImages = (
|
||||
docker: Dockerode,
|
||||
images: BuiltImage[],
|
||||
serviceImages: Release['serviceImages'],
|
||||
): Promise<TaggedImage[]> =>
|
||||
Promise.all(
|
||||
images.map(function (d) {
|
||||
const serviceImage = serviceImages[d.serviceName];
|
||||
@ -272,25 +183,24 @@ export const tagServiceImages = (docker, images, serviceImages) =>
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {*} sdk
|
||||
* @param {import('./logger')} logger
|
||||
* @param {number} appID
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export const getPreviousRepos = (sdk, logger, appID) =>
|
||||
export const getPreviousRepos = (
|
||||
sdk: SDK.BalenaSDK,
|
||||
logger: Logger,
|
||||
appID: number,
|
||||
): Promise<string[]> =>
|
||||
sdk.pine
|
||||
.get({
|
||||
.get<SDK.Release>({
|
||||
resource: 'release',
|
||||
options: {
|
||||
$select: 'id',
|
||||
$filter: {
|
||||
belongs_to__application: appID,
|
||||
status: 'success',
|
||||
},
|
||||
$select: ['id'],
|
||||
$expand: {
|
||||
contains__image: {
|
||||
$expand: 'image',
|
||||
$select: 'image',
|
||||
$expand: { image: { $select: 'is_stored_at__image_location' } },
|
||||
},
|
||||
},
|
||||
$orderby: 'id desc',
|
||||
@ -300,8 +210,11 @@ export const getPreviousRepos = (sdk, logger, appID) =>
|
||||
.then(function (release) {
|
||||
// grab all images from the latest release, return all image locations in the registry
|
||||
if (release.length > 0) {
|
||||
const images = release[0].contains__image;
|
||||
const { getRegistryAndName } = require('resin-multibuild');
|
||||
const images = release[0].contains__image as Array<{
|
||||
image: [SDK.Image];
|
||||
}>;
|
||||
const { getRegistryAndName } =
|
||||
require('resin-multibuild') as typeof import('resin-multibuild');
|
||||
return Promise.all(
|
||||
images.map(function (d) {
|
||||
const imageName = d.image[0].is_stored_at__image_location || '';
|
||||
@ -321,21 +234,13 @@ export const getPreviousRepos = (sdk, logger, appID) =>
|
||||
return [];
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {*} sdk
|
||||
* @param {string} tokenAuthEndpoint
|
||||
* @param {string} registry
|
||||
* @param {string[]} images
|
||||
* @param {string[]} previousRepos
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export const authorizePush = function (
|
||||
sdk,
|
||||
tokenAuthEndpoint,
|
||||
registry,
|
||||
images,
|
||||
previousRepos,
|
||||
) {
|
||||
sdk: SDK.BalenaSDK,
|
||||
tokenAuthEndpoint: string,
|
||||
registry: string,
|
||||
images: string[],
|
||||
previousRepos: string[],
|
||||
): Promise<string> {
|
||||
if (!Array.isArray(images)) {
|
||||
images = [images];
|
||||
}
|
||||
@ -356,17 +261,20 @@ export const authorizePush = function (
|
||||
|
||||
// utilities
|
||||
|
||||
const renderProgressBar = function (percentage, stepCount) {
|
||||
const _ = require('lodash');
|
||||
const renderProgressBar = function (percentage: number, stepCount: number) {
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
percentage = _.clamp(percentage, 0, 100);
|
||||
const barCount = Math.floor((stepCount * percentage) / 100);
|
||||
const spaceCount = stepCount - barCount;
|
||||
const bar = `[${_.repeat('=', barCount)}>${_.repeat(' ', spaceCount)}]`;
|
||||
return `${bar} ${_.padStart(percentage, 3)}%`;
|
||||
return `${bar} ${_.padStart(`${percentage}`, 3)}%`;
|
||||
};
|
||||
|
||||
export const pushProgressRenderer = function (tty, prefix) {
|
||||
const fn = function (e) {
|
||||
export const pushProgressRenderer = function (
|
||||
tty: ReturnType<typeof import('./tty')>,
|
||||
prefix: string,
|
||||
): ProgressCallback & { end: () => void } {
|
||||
const fn: ProgressCallback & { end: () => void } = function (e) {
|
||||
const { error, percentage } = e;
|
||||
if (error != null) {
|
||||
throw new Error(error);
|
||||
@ -380,14 +288,39 @@ export const pushProgressRenderer = function (tty, prefix) {
|
||||
return fn;
|
||||
};
|
||||
|
||||
export class BuildProgressUI {
|
||||
constructor(tty, descriptors) {
|
||||
export class BuildProgressUI implements Renderer {
|
||||
public streams;
|
||||
private _prefix;
|
||||
private _prefixWidth;
|
||||
private _tty;
|
||||
private _services;
|
||||
private _startTime: undefined | number;
|
||||
private _ended;
|
||||
private _serviceToDataMap: Dictionary<{
|
||||
status?: string;
|
||||
progress?: number;
|
||||
error?: Error;
|
||||
}> = {};
|
||||
private _cancelled;
|
||||
private _spinner;
|
||||
private _runloop:
|
||||
| undefined
|
||||
| ReturnType<typeof import('./compose_ts').createRunLoop>;
|
||||
|
||||
// these are to handle window wrapping
|
||||
private _maxLineWidth: undefined | number;
|
||||
private _lineWidths: number[] = [];
|
||||
|
||||
constructor(
|
||||
tty: ReturnType<typeof import('./tty')>,
|
||||
descriptors: ImageDescriptor[],
|
||||
) {
|
||||
this._handleEvent = this._handleEvent.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
this.end = this.end.bind(this);
|
||||
this._display = this._display.bind(this);
|
||||
const _ = require('lodash');
|
||||
const through = require('through2');
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
const through = require('through2') as typeof import('through2');
|
||||
|
||||
const eventHandler = this._handleEvent;
|
||||
const services = _.map(descriptors, 'serviceName');
|
||||
@ -405,7 +338,6 @@ export class BuildProgressUI {
|
||||
.value();
|
||||
|
||||
this._tty = tty;
|
||||
this._serviceToDataMap = {};
|
||||
this._services = services;
|
||||
|
||||
// Logger magically prefixes the log line with [Build] etc., but it doesn't
|
||||
@ -415,22 +347,22 @@ export class BuildProgressUI {
|
||||
|
||||
const offset = 10; // account for escape sequences inserted for colouring
|
||||
this._prefixWidth =
|
||||
offset + prefix.length + _.max(_.map(services, 'length'));
|
||||
offset + prefix.length + _.max(_.map(services, (s) => s.length))!;
|
||||
this._prefix = prefix;
|
||||
|
||||
// these are to handle window wrapping
|
||||
this._maxLineWidth = null;
|
||||
this._lineWidths = [];
|
||||
|
||||
this._startTime = null;
|
||||
this._ended = false;
|
||||
this._cancelled = false;
|
||||
this._spinner = require('./compose_ts').createSpinner();
|
||||
this._spinner = (
|
||||
require('./compose_ts') as typeof import('./compose_ts')
|
||||
).createSpinner();
|
||||
|
||||
this.streams = streams;
|
||||
}
|
||||
|
||||
_handleEvent(service, event) {
|
||||
_handleEvent(
|
||||
service: string,
|
||||
event: { status?: string; progress?: number; error?: Error },
|
||||
) {
|
||||
this._serviceToDataMap[service] = event;
|
||||
}
|
||||
|
||||
@ -439,17 +371,19 @@ export class BuildProgressUI {
|
||||
this._services.forEach((service) => {
|
||||
this.streams[service].write({ status: 'Preparing...' });
|
||||
});
|
||||
this._runloop = require('./compose_ts').createRunLoop(this._display);
|
||||
this._runloop = (
|
||||
require('./compose_ts') as typeof import('./compose_ts')
|
||||
).createRunLoop(this._display);
|
||||
this._startTime = Date.now();
|
||||
}
|
||||
|
||||
end(summary = null) {
|
||||
end(summary?: Dictionary<string>) {
|
||||
if (this._ended) {
|
||||
return;
|
||||
}
|
||||
this._ended = true;
|
||||
this._runloop?.end();
|
||||
this._runloop = null;
|
||||
this._runloop = undefined;
|
||||
|
||||
this._clear();
|
||||
this._renderStatus(true);
|
||||
@ -470,7 +404,7 @@ export class BuildProgressUI {
|
||||
}
|
||||
|
||||
_getServiceSummary() {
|
||||
const _ = require('lodash');
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
|
||||
const services = this._services;
|
||||
const serviceToDataMap = this._serviceToDataMap;
|
||||
@ -497,11 +431,11 @@ export class BuildProgressUI {
|
||||
.value();
|
||||
}
|
||||
|
||||
_renderStatus(end) {
|
||||
end ??= false;
|
||||
|
||||
const moment = require('moment');
|
||||
require('moment-duration-format')(moment);
|
||||
_renderStatus(end = false) {
|
||||
const moment = require('moment') as typeof import('moment');
|
||||
(
|
||||
require('moment-duration-format') as typeof import('moment-duration-format')
|
||||
)(moment);
|
||||
|
||||
this._tty.clearLine();
|
||||
this._tty.write(this._prefix);
|
||||
@ -526,11 +460,11 @@ export class BuildProgressUI {
|
||||
}
|
||||
}
|
||||
|
||||
_renderSummary(serviceToStrMap) {
|
||||
const _ = require('lodash');
|
||||
_renderSummary(serviceToStrMap: Dictionary<string>) {
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
const chalk = getChalk();
|
||||
const truncate = require('cli-truncate');
|
||||
const strlen = require('string-width');
|
||||
const truncate = require('cli-truncate') as typeof import('cli-truncate');
|
||||
const strlen = require('string-width') as typeof import('string-width');
|
||||
|
||||
this._services.forEach((service, index) => {
|
||||
let str = _.padEnd(this._prefix + chalk.bold(service), this._prefixWidth);
|
||||
@ -546,13 +480,23 @@ export class BuildProgressUI {
|
||||
}
|
||||
}
|
||||
|
||||
export class BuildProgressInline {
|
||||
constructor(outStream, descriptors) {
|
||||
export class BuildProgressInline implements Renderer {
|
||||
public streams;
|
||||
private _prefixWidth;
|
||||
private _outStream;
|
||||
private _services;
|
||||
private _startTime: number | undefined;
|
||||
private _ended;
|
||||
|
||||
constructor(
|
||||
outStream: NodeJS.ReadWriteStream,
|
||||
descriptors: Array<{ serviceName: string }>,
|
||||
) {
|
||||
this.start = this.start.bind(this);
|
||||
this.end = this.end.bind(this);
|
||||
this._renderEvent = this._renderEvent.bind(this);
|
||||
const _ = require('lodash');
|
||||
const through = require('through2');
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
const through = require('through2') as typeof import('through2');
|
||||
|
||||
const services = _.map(descriptors, 'serviceName');
|
||||
const eventHandler = this._renderEvent;
|
||||
@ -569,10 +513,9 @@ export class BuildProgressInline {
|
||||
.value();
|
||||
|
||||
const offset = 10; // account for escape sequences inserted for colouring
|
||||
this._prefixWidth = offset + _.max(_.map(services, 'length'));
|
||||
this._prefixWidth = offset + _.max(_.map(services, (s) => s.length))!;
|
||||
this._outStream = outStream;
|
||||
this._services = services;
|
||||
this._startTime = null;
|
||||
this._ended = false;
|
||||
|
||||
this.streams = streams;
|
||||
@ -586,9 +529,11 @@ export class BuildProgressInline {
|
||||
this._startTime = Date.now();
|
||||
}
|
||||
|
||||
end(summary = null) {
|
||||
const moment = require('moment');
|
||||
require('moment-duration-format')(moment);
|
||||
end(summary?: Dictionary<string>) {
|
||||
const moment = require('moment') as typeof import('moment');
|
||||
(
|
||||
require('moment-duration-format') as typeof import('moment-duration-format')
|
||||
)(moment);
|
||||
|
||||
if (this._ended) {
|
||||
return;
|
||||
@ -616,8 +561,8 @@ export class BuildProgressInline {
|
||||
this._outStream.write(`Built ${serviceStr} in ${durationStr}\n`);
|
||||
}
|
||||
|
||||
_renderEvent(service, event) {
|
||||
const _ = require('lodash');
|
||||
_renderEvent(service: string, event: { status?: string; error?: Error }) {
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
|
||||
const str = (function () {
|
||||
const { status, error } = event;
|
@ -28,6 +28,7 @@ import type {
|
||||
ImageDescriptor,
|
||||
} from 'resin-compose-parse';
|
||||
import type * as MultiBuild from 'resin-multibuild';
|
||||
import * as semver from 'semver';
|
||||
import type { Duplex, Readable } from 'stream';
|
||||
import type { Pack } from 'tar-stream';
|
||||
|
||||
@ -43,7 +44,6 @@ import {
|
||||
import type { DeviceInfo } from './device/api';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
import { isV13 } from './version';
|
||||
import { exists } from './which';
|
||||
|
||||
const allowedContractTypes = ['sw.application', 'sw.block'];
|
||||
@ -105,8 +105,6 @@ export async function applyReleaseTagKeysAndValues(
|
||||
|
||||
const LOG_LENGTH_MAX = 512 * 1024; // 512KB
|
||||
const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml'];
|
||||
const hr =
|
||||
'----------------------------------------------------------------------';
|
||||
|
||||
/**
|
||||
* high-level function resolving a project and creating a composition out
|
||||
@ -238,7 +236,7 @@ interface BuildTaskPlus extends MultiBuild.BuildTask {
|
||||
logBuffer?: string[];
|
||||
}
|
||||
|
||||
interface Renderer {
|
||||
export interface Renderer {
|
||||
start: () => void;
|
||||
end: (buildSummaryByService?: Dictionary<string>) => void;
|
||||
streams: Dictionary<NodeJS.ReadWriteStream>;
|
||||
@ -257,7 +255,6 @@ export interface BuildProjectOpts {
|
||||
inlineLogs?: boolean;
|
||||
convertEol: boolean;
|
||||
dockerfilePath?: string;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
multiDockerignore: boolean;
|
||||
}
|
||||
|
||||
@ -748,43 +745,19 @@ export function isBuildConfig(
|
||||
* Create a tar stream out of the local filesystem at the given directory,
|
||||
* while optionally applying file filters such as '.dockerignore' and
|
||||
* optionally converting text file line endings (CRLF to LF).
|
||||
* @param dir Source directory
|
||||
* @param param Options
|
||||
* @returns Readable stream
|
||||
* @param dir Project directory (the '--source' command line option)
|
||||
* @param param TarDirectoryOptions
|
||||
* @returns Readable stream (to be sent to the Docker Engine)
|
||||
*/
|
||||
export async function tarDirectory(
|
||||
dir: string,
|
||||
param: TarDirectoryOptions,
|
||||
): Promise<import('stream').Readable> {
|
||||
const { nogitignore = false } = param; // v13: delete this line
|
||||
if (isV13() || nogitignore) {
|
||||
return newTarDirectory(dir, param);
|
||||
} else {
|
||||
return (await import('./compose')).originalTarDirectory(dir, param);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tar stream out of the local filesystem at the given directory,
|
||||
* while optionally applying file filters such as '.dockerignore' and
|
||||
* optionally converting text file line endings (CRLF to LF).
|
||||
* @param dir Source directory
|
||||
* @param param Options
|
||||
* @returns Readable stream
|
||||
*/
|
||||
async function newTarDirectory(
|
||||
dir: string,
|
||||
{
|
||||
composition,
|
||||
convertEol = false,
|
||||
multiDockerignore = false,
|
||||
nogitignore = false, // v13: delete this line
|
||||
preFinalizeCallback,
|
||||
}: TarDirectoryOptions,
|
||||
): Promise<import('stream').Readable> {
|
||||
if (!isV13()) {
|
||||
require('assert').strict.equal(nogitignore, true);
|
||||
}
|
||||
const { filterFilesWithDockerignore } = await import('./ignore');
|
||||
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
|
||||
|
||||
@ -905,48 +878,6 @@ function printDockerignoreWarn(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a deprecation warning if any '.gitignore' or '.dockerignore' file is
|
||||
* found and the --gitignore (-g) option has been provided (v11 compatibility).
|
||||
* @param dockerignoreFile Absolute path to a .dockerignore file
|
||||
* @param gitignoreFiles Array of absolute paths to .gitginore files
|
||||
*
|
||||
* v13: delete this function
|
||||
*/
|
||||
export function printGitignoreWarn(
|
||||
dockerignoreFile: string,
|
||||
gitignoreFiles: string[],
|
||||
) {
|
||||
if (isV13()) {
|
||||
return;
|
||||
}
|
||||
const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter((e) => e);
|
||||
if (ignoreFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
const msg = [' ', hr, 'Using file ignore patterns from:'];
|
||||
msg.push(...ignoreFiles.map((e) => `* ${e}`));
|
||||
if (gitignoreFiles.length) {
|
||||
msg.push(stripIndent`
|
||||
.gitignore files are being considered because the --gitignore option was used.
|
||||
This option is deprecated and will be removed in the next major version release.
|
||||
For more information, see 'balena help ${Logger.command}'.
|
||||
`);
|
||||
msg.push(hr);
|
||||
Logger.getLogger().logWarn(msg.join('\n'));
|
||||
} else if (dockerignoreFile && process.platform === 'win32') {
|
||||
msg.push(stripIndent`
|
||||
The --gitignore option was used, but no .gitignore files were found.
|
||||
The --gitignore option is deprecated and will be removed in the next major
|
||||
version release. It prevents the use of a better dockerignore parser and
|
||||
filter library that fixes several issues on Windows and improves compatibility
|
||||
with 'docker build'. For more information, see 'balena help ${Logger.command}'.
|
||||
`);
|
||||
msg.push(hr);
|
||||
Logger.getLogger().logWarn(msg.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the "build secrets" feature is being used and, if so,
|
||||
* verify that the target docker daemon is balenaEngine. If the
|
||||
@ -1418,9 +1349,6 @@ async function pushServiceImages(
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: This should be shared between the CLI & the Builder
|
||||
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
|
||||
|
||||
export async function deployProject(
|
||||
docker: Dockerode,
|
||||
logger: Logger,
|
||||
@ -1443,10 +1371,10 @@ export async function deployProject(
|
||||
|
||||
const contractPath = path.join(projectPath, 'balena.yml');
|
||||
const contract = await getContractContent(contractPath);
|
||||
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
|
||||
if (contract?.version && !semver.valid(contract.version)) {
|
||||
throw new ExpectedError(stripIndent`\
|
||||
Error: expected the version field in "${contractPath}"
|
||||
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
|
||||
Error: the version field in "${contractPath}"
|
||||
is not a valid semver`);
|
||||
}
|
||||
|
||||
const $release = await runSpinner(
|
||||
@ -1711,6 +1639,7 @@ function truncateString(str: string, len: number): string {
|
||||
return str.slice(0, str.lastIndexOf('\n'));
|
||||
}
|
||||
|
||||
// TODO: docker-compose naming
|
||||
export const composeCliFlags: flags.Input<ComposeCliFlags> = {
|
||||
emulated: flags.boolean({
|
||||
description:
|
||||
@ -1721,6 +1650,10 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
|
||||
description:
|
||||
'Alternative Dockerfile name/path, relative to the source folder',
|
||||
}),
|
||||
dockercompose: flags.string({
|
||||
description:
|
||||
'Alternative docker-compose.yml name in the source root folder',
|
||||
}),
|
||||
logs: flags.boolean({
|
||||
description:
|
||||
'No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.',
|
||||
@ -1729,21 +1662,6 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
|
||||
description:
|
||||
'Hide the image build log output (produce less verbose output)',
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
gitignore: flags.boolean({
|
||||
description: stripIndent`
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||
until your project can be adapted.`,
|
||||
char: 'g',
|
||||
}),
|
||||
nogitignore: flags.boolean({
|
||||
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
|
||||
char: 'G',
|
||||
}),
|
||||
}),
|
||||
'multi-dockerignore': flags.boolean({
|
||||
description:
|
||||
'Have each service use its own .dockerignore file. See "balena help build".',
|
||||
@ -1758,10 +1676,6 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
|
||||
'Path to a YAML or JSON file with passwords for a private Docker registry',
|
||||
char: 'R',
|
||||
}),
|
||||
'convert-eol': flags.boolean({
|
||||
description: 'No-op and deprecated since balena CLI v12.0.0',
|
||||
char: 'l',
|
||||
}),
|
||||
'noconvert-eol': flags.boolean({
|
||||
description:
|
||||
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).",
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import * as semver from 'balena-semver';
|
||||
import { getBalenaSdk } from './lazy';
|
||||
import { getBalenaSdk, stripIndent } from './lazy';
|
||||
|
||||
export interface ImgConfig {
|
||||
applicationName: string;
|
||||
@ -134,3 +134,34 @@ export function generateDeviceConfig(
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Chech whether the `--dev` option of commands related to OS configuration
|
||||
* such as `os configure` and `config generate` is compatible with a given
|
||||
* balenaOS version, and print a warning regarding the consequences of using
|
||||
* that option.
|
||||
*/
|
||||
export async function validateDevOptionAndWarn(
|
||||
dev?: boolean,
|
||||
version?: string,
|
||||
logger?: import('./logger'),
|
||||
) {
|
||||
if (!dev) {
|
||||
return;
|
||||
}
|
||||
if (version && /\bprod\b/.test(version)) {
|
||||
const { ExpectedError } = await import('../errors');
|
||||
throw new ExpectedError(
|
||||
`Error: The '--dev' option conflicts with production balenaOS version '${version}'`,
|
||||
);
|
||||
}
|
||||
if (!logger) {
|
||||
const Logger = await import('./logger');
|
||||
logger = Logger.getLogger();
|
||||
}
|
||||
logger.logInfo(stripIndent`
|
||||
The '--dev' option is being used to configure a balenaOS image in development mode.
|
||||
Please note that development mode allows unauthenticated, passwordless root ssh access
|
||||
and exposes network ports such as 2375 that allows unencrypted access to balenaEngine.
|
||||
Therefore, development mode should only be used in private, trusted local networks.`);
|
||||
}
|
||||
|
@ -17,15 +17,27 @@
|
||||
|
||||
import { getVisuals } from './lazy';
|
||||
import { promisify } from 'util';
|
||||
import type * as Dockerode from 'dockerode';
|
||||
import type Logger = require('./logger');
|
||||
import type { Request } from 'request';
|
||||
|
||||
const getBuilderPushEndpoint = function (baseUrl, owner, app) {
|
||||
const querystring = require('querystring');
|
||||
const getBuilderPushEndpoint = function (
|
||||
baseUrl: string,
|
||||
owner: string,
|
||||
app: string,
|
||||
) {
|
||||
const querystring = require('querystring') as typeof import('querystring');
|
||||
const args = querystring.stringify({ owner, app });
|
||||
return `https://builder.${baseUrl}/v1/push?${args}`;
|
||||
};
|
||||
|
||||
const getBuilderLogPushEndpoint = function (baseUrl, buildId, owner, app) {
|
||||
const querystring = require('querystring');
|
||||
const getBuilderLogPushEndpoint = function (
|
||||
baseUrl: string,
|
||||
buildId: number,
|
||||
owner: string,
|
||||
app: string,
|
||||
) {
|
||||
const querystring = require('querystring') as typeof import('querystring');
|
||||
const args = querystring.stringify({ owner, app, buildId });
|
||||
return `https://builder.${baseUrl}/v1/pushLogs?${args}`;
|
||||
};
|
||||
@ -35,32 +47,37 @@ const getBuilderLogPushEndpoint = function (baseUrl, buildId, owner, app) {
|
||||
* @param {string} imageId
|
||||
* @param {string} bufferFile
|
||||
*/
|
||||
const bufferImage = function (docker, imageId, bufferFile) {
|
||||
const streamUtils = require('./streams');
|
||||
const bufferImage = function (
|
||||
docker: Dockerode,
|
||||
imageId: string,
|
||||
bufferFile: string,
|
||||
): Promise<NodeJS.ReadableStream & { length: number }> {
|
||||
const streamUtils = require('./streams') as typeof import('./streams');
|
||||
|
||||
const image = docker.getImage(imageId);
|
||||
const sizePromise = image.inspect().then((img) => img.Size);
|
||||
|
||||
return Promise.all([image.get(), sizePromise]).then(
|
||||
([imageStream, imageSize]) =>
|
||||
streamUtils.buffer(imageStream, bufferFile).then((bufferedStream) => {
|
||||
// @ts-ignore adding an extra property
|
||||
bufferedStream.length = imageSize;
|
||||
return bufferedStream;
|
||||
}),
|
||||
streamUtils
|
||||
.buffer(imageStream, bufferFile)
|
||||
.then((bufferedStream: NodeJS.ReadableStream & { length?: number }) => {
|
||||
bufferedStream.length = imageSize;
|
||||
return bufferedStream as NodeJS.ReadableStream & { length: number };
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const showPushProgress = function (message) {
|
||||
const showPushProgress = function (message: string) {
|
||||
const visuals = getVisuals();
|
||||
const progressBar = new visuals.Progress(message);
|
||||
progressBar.update({ percentage: 0 });
|
||||
return progressBar;
|
||||
};
|
||||
|
||||
const uploadToPromise = (uploadRequest, logger) =>
|
||||
new Promise(function (resolve, reject) {
|
||||
const handleMessage = function (data) {
|
||||
const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
|
||||
new Promise<{ buildId: number }>(function (resolve, reject) {
|
||||
uploadRequest.on('error', reject).on('data', function handleMessage(data) {
|
||||
let obj;
|
||||
data = data.toString();
|
||||
logger.logDebug(`Received data: ${data}`);
|
||||
@ -86,25 +103,24 @@ const uploadToPromise = (uploadRequest, logger) =>
|
||||
default:
|
||||
reject(new Error(`Received unexpected reply from remote: ${data}`));
|
||||
}
|
||||
};
|
||||
|
||||
uploadRequest.on('error', reject).on('data', handleMessage);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns {Promise<{ buildId: number }>}
|
||||
*/
|
||||
const uploadImage = function (
|
||||
imageStream,
|
||||
token,
|
||||
username,
|
||||
url,
|
||||
appName,
|
||||
logger,
|
||||
) {
|
||||
const request = require('request');
|
||||
const progressStream = require('progress-stream');
|
||||
const zlib = require('zlib');
|
||||
imageStream: NodeJS.ReadableStream & { length: number },
|
||||
token: string,
|
||||
username: string,
|
||||
url: string,
|
||||
appName: string,
|
||||
logger: Logger,
|
||||
): Promise<{ buildId: number }> {
|
||||
const request = require('request') as typeof import('request');
|
||||
const progressStream =
|
||||
require('progress-stream') as typeof import('progress-stream');
|
||||
const zlib = require('zlib') as typeof import('zlib');
|
||||
|
||||
// Need to strip off the newline
|
||||
const progressMessage = logger
|
||||
@ -143,8 +159,15 @@ const uploadImage = function (
|
||||
return uploadToPromise(uploadRequest, logger);
|
||||
};
|
||||
|
||||
const uploadLogs = function (logs, token, url, buildId, username, appName) {
|
||||
const request = require('request');
|
||||
const uploadLogs = function (
|
||||
logs: string,
|
||||
token: string,
|
||||
url: string,
|
||||
buildId: number,
|
||||
username: string,
|
||||
appName: string,
|
||||
) {
|
||||
const request = require('request') as typeof import('request');
|
||||
return request.post({
|
||||
json: true,
|
||||
url: getBuilderLogPushEndpoint(url, buildId, username, appName),
|
||||
@ -156,25 +179,24 @@ const uploadLogs = function (logs, token, url, buildId, username, appName) {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('dockerode')} docker
|
||||
* @param {import('./logger')} logger
|
||||
* @param {string} token
|
||||
* @param {string} username
|
||||
* @param {string} url
|
||||
* @param {{appName: string; imageName: string; buildLogs: string; shouldUploadLogs: boolean}} opts
|
||||
* - appName: the name of the app to deploy to
|
||||
* - imageName: the name of the image to deploy
|
||||
* - buildLogs: a string with build output
|
||||
*/
|
||||
export const deployLegacy = async function (
|
||||
docker,
|
||||
logger,
|
||||
token,
|
||||
username,
|
||||
url,
|
||||
opts,
|
||||
) {
|
||||
const tmp = require('tmp');
|
||||
docker: Dockerode,
|
||||
logger: Logger,
|
||||
token: string,
|
||||
username: string,
|
||||
url: string,
|
||||
opts: {
|
||||
appName: string;
|
||||
imageName: string;
|
||||
buildLogs: string;
|
||||
shouldUploadLogs: boolean;
|
||||
},
|
||||
): Promise<number> {
|
||||
const tmp = require('tmp') as typeof import('tmp');
|
||||
const tmpNameAsync = promisify(tmp.tmpName);
|
||||
|
||||
// Ensure the tmp files gets deleted
|
||||
@ -195,8 +217,8 @@ export const deployLegacy = async function (
|
||||
// has occured before any data was written) this call will throw an
|
||||
// ugly error, just suppress it
|
||||
|
||||
require('fs')
|
||||
.promises.unlink(bufferFile)
|
||||
(require('fs') as typeof import('fs')).promises
|
||||
.unlink(bufferFile)
|
||||
.catch(() => undefined),
|
||||
);
|
||||
|
@ -59,7 +59,6 @@ export interface DeviceDeployOptions {
|
||||
registrySecrets: RegistrySecrets;
|
||||
multiDockerignore: boolean;
|
||||
nocache: boolean;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
noParentCheck: boolean;
|
||||
nolive: boolean;
|
||||
pull: boolean;
|
||||
@ -184,7 +183,6 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
convertEol: opts.convertEol,
|
||||
dockerfilePath: opts.dockerfilePath,
|
||||
multiDockerignore: opts.multiDockerignore,
|
||||
nogitignore: opts.nogitignore, // v13: delete this line
|
||||
noParentCheck: opts.noParentCheck,
|
||||
projectName: 'local',
|
||||
projectPath: opts.source,
|
||||
@ -204,7 +202,6 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
composition: project.composition,
|
||||
convertEol: opts.convertEol,
|
||||
multiDockerignore: opts.multiDockerignore,
|
||||
nogitignore: opts.nogitignore, // v13: delete this line
|
||||
});
|
||||
globalLogger.logDebug(`Tarring complete in ${Date.now() - tarStartTime} ms`);
|
||||
|
||||
@ -435,7 +432,6 @@ export async function rebuildSingleTask(
|
||||
composition,
|
||||
convertEol: opts.convertEol,
|
||||
multiDockerignore: opts.multiDockerignore,
|
||||
nogitignore: opts.nogitignore, // v13: delete this line
|
||||
});
|
||||
|
||||
const task = _.find(
|
||||
|
@ -13,89 +13,140 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import type { ContainerInfo } from 'dockerode';
|
||||
|
||||
import { ExpectedError } from '../../errors';
|
||||
import { stripIndent } from '../lazy';
|
||||
|
||||
export interface DeviceSSHOpts {
|
||||
address: string;
|
||||
port?: number;
|
||||
import {
|
||||
findBestUsernameForDevice,
|
||||
getRemoteCommandOutput,
|
||||
runRemoteCommand,
|
||||
SshRemoteCommandOpts,
|
||||
} from '../ssh';
|
||||
|
||||
export interface DeviceSSHOpts extends SshRemoteCommandOpts {
|
||||
forceTTY?: boolean;
|
||||
verbose: boolean;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
export const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
|
||||
const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
|
||||
|
||||
/**
|
||||
* List the running containers on the device over ssh, and return the full
|
||||
* container name that matches the given service name.
|
||||
*
|
||||
* Note: In the past, two other approaches were implemented for this function:
|
||||
*
|
||||
* - Obtaining container IDs through a supervisor API call:
|
||||
* '/supervisor/v2/containerId' endpoint, via cloud.
|
||||
* - Obtaining container IDs using 'dockerode' connected directly to
|
||||
* balenaEngine on a device, TCP port 2375.
|
||||
*
|
||||
* The problem with using the supervisor API is that it means that 'balena ssh'
|
||||
* becomes dependent on the supervisor being up an running, but sometimes ssh
|
||||
* is needed to investigate devices issues where the supervisor has got into
|
||||
* trouble (e.g. supervisor in restart loop). This is the subject of CLI issue
|
||||
* https://github.com/balena-io/balena-cli/issues/1560 .
|
||||
*
|
||||
* The problem with using dockerode to connect directly to port 2375 (balenaEngine)
|
||||
* is that it only works with development variants of balenaOS. Production variants
|
||||
* block access to port 2375 for security reasons. 'balena ssh' should support
|
||||
* production variants as well, especially after balenaOS v2.44.0 that introduced
|
||||
* support for using the cloud account username for ssh authentication.
|
||||
*
|
||||
* Overall, the most reliable approach is to run 'balena-engine ps' over ssh.
|
||||
* It is OK to depend on balenaEngine because ssh to a container is implemented
|
||||
* through 'balena-engine exec' anyway, and of course it is OK to depend on ssh
|
||||
* itself.
|
||||
*/
|
||||
export async function getContainerIdForService(
|
||||
opts: SshRemoteCommandOpts & { service: string; deviceUuid?: string },
|
||||
): Promise<string> {
|
||||
opts.cmd = `"${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`;
|
||||
if (opts.deviceUuid) {
|
||||
// If a device UUID is given, perform ssh via cloud proxy 'host' command
|
||||
opts.cmd = `host ${opts.deviceUuid} ${opts.cmd}`;
|
||||
}
|
||||
|
||||
const psLines: string[] = (
|
||||
await getRemoteCommandOutput({ ...opts, stderr: 'inherit' })
|
||||
).stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((l) => l);
|
||||
|
||||
const { escapeRegExp } = await import('lodash');
|
||||
const regex = new RegExp(`(?:^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
|
||||
// Old balenaOS container name pattern:
|
||||
// main_1234567_2345678
|
||||
// New balenaOS container name patterns:
|
||||
// main_1234567_2345678_a000b111c222d333e444f555a666b777
|
||||
// main_1_1_localrelease
|
||||
const nameRegex = /(?:^|\/)([a-zA-Z0-9_-]+)_\d+_\d+(?:_.+)?$/;
|
||||
|
||||
const serviceNames: string[] = [];
|
||||
const containerNames: string[] = [];
|
||||
let containerId: string | undefined;
|
||||
|
||||
// sample psLine: 'b603c74e951e bar_4587562_2078151_3261c9d4c22f2c53a5267be459c89990'
|
||||
for (const psLine of psLines) {
|
||||
const [cId, name] = psLine.split(' ');
|
||||
if (cId && name) {
|
||||
if (regex.test(name)) {
|
||||
containerNames.push(name);
|
||||
containerId = cId;
|
||||
}
|
||||
const match = name.match(nameRegex);
|
||||
if (match) {
|
||||
serviceNames.push(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (containerNames.length > 1) {
|
||||
const [s, d] = [opts.service, opts.deviceUuid || opts.hostname];
|
||||
throw new ExpectedError(stripIndent`
|
||||
Found more than one container matching service name "${s}" on device "${d}":
|
||||
${containerNames.join(', ')}
|
||||
Use different service names to avoid ambiguity.
|
||||
`);
|
||||
}
|
||||
if (!containerId) {
|
||||
const [s, d] = [opts.service, opts.deviceUuid || opts.hostname];
|
||||
throw new ExpectedError(
|
||||
`Could not find a container matching service name "${s}" on device "${d}".${
|
||||
serviceNames.length > 0
|
||||
? `\nAvailable services:\n\t${serviceNames.join('\n\t')}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
return containerId;
|
||||
}
|
||||
|
||||
export async function performLocalDeviceSSH(
|
||||
opts: DeviceSSHOpts,
|
||||
): Promise<void> {
|
||||
const { escapeRegExp, reduce } = await import('lodash');
|
||||
const { spawnSshAndThrowOnError } = await import('../ssh');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
// Before we started using `findBestUsernameForDevice`, we tried the approach
|
||||
// of attempting ssh with the 'root' username first and, if that failed, then
|
||||
// attempting ssh with a regular user (balenaCloud username). The problem with
|
||||
// that approach was that it would print the following message to the console:
|
||||
// "root@192.168.1.36: Permission denied (publickey)"
|
||||
// ... right before having success as a regular user, which looked broken or
|
||||
// confusing from users' point of view. Capturing stderr to prevent that
|
||||
// message from being printed is tricky because the messages printed to stderr
|
||||
// may include the stderr output of remote commands that are of interest to
|
||||
// the user.
|
||||
const username = await findBestUsernameForDevice(opts.hostname, opts.port);
|
||||
let cmd = '';
|
||||
|
||||
let command = '';
|
||||
|
||||
if (opts.service != null) {
|
||||
// Get the containers which are on-device. Currently we
|
||||
// are single application, which means we can assume any
|
||||
// container which fulfills the form of
|
||||
// $serviceName_$appId_$releaseId is what we want. Once
|
||||
// we have multi-app, we should show a dialog which
|
||||
// allows the user to choose the correct container
|
||||
|
||||
const Docker = await import('dockerode');
|
||||
const docker = new Docker({
|
||||
host: opts.address,
|
||||
port: 2375,
|
||||
if (opts.service) {
|
||||
const containerId = await getContainerIdForService({
|
||||
...opts,
|
||||
service: opts.service,
|
||||
username,
|
||||
});
|
||||
|
||||
const regex = new RegExp(`(^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
|
||||
const nameRegex = /\/?([a-zA-Z0-9_-]+)_\d+_\d+/;
|
||||
let allContainers: ContainerInfo[];
|
||||
try {
|
||||
allContainers = await docker.listContainers();
|
||||
} catch (_e) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Could not access docker daemon on device ${opts.address}.
|
||||
Please ensure the device is in local mode.`);
|
||||
}
|
||||
|
||||
const serviceNames: string[] = [];
|
||||
const containers: Array<{ id: string; name: string }> = [];
|
||||
for (const container of allContainers) {
|
||||
for (const name of container.Names) {
|
||||
if (regex.test(name)) {
|
||||
containers.push({ id: container.Id, name });
|
||||
break;
|
||||
}
|
||||
const match = name.match(nameRegex);
|
||||
if (match) {
|
||||
serviceNames.push(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containers.length === 0) {
|
||||
throw new ExpectedError(
|
||||
`Could not find a service on device with name ${opts.service}. ${
|
||||
serviceNames.length > 0
|
||||
? `Available services:\n${reduce(
|
||||
serviceNames,
|
||||
(str, name) => `${str}\t${name}\n`,
|
||||
'',
|
||||
)}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
if (containers.length > 1) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Found more than one container matching service name "${opts.service}":
|
||||
${containers.map((container) => container.name).join(', ')}
|
||||
Use different service names to avoid ambiguity.
|
||||
`);
|
||||
}
|
||||
|
||||
const containerId = containers[0].id;
|
||||
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
|
||||
// stdin (fd=0) is not a tty when data is piped in, for example
|
||||
// echo 'ls -la; exit;' | balena ssh 192.168.0.20 service1
|
||||
@ -103,17 +154,8 @@ export async function performLocalDeviceSSH(
|
||||
// https://assets.balena.io/newsletter/2020-01/pipe.png
|
||||
const isTTY = !!opts.forceTTY || (await import('tty')).isatty(0);
|
||||
const ttyFlag = isTTY ? '-t' : '';
|
||||
command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
|
||||
cmd = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
|
||||
}
|
||||
|
||||
return spawnSshAndThrowOnError([
|
||||
...(opts.verbose ? ['-vvv'] : []),
|
||||
'-t',
|
||||
...['-p', opts.port ? opts.port.toString() : '22222'],
|
||||
...['-o', 'LogLevel=ERROR'],
|
||||
...['-o', 'StrictHostKeyChecking=no'],
|
||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||
`root@${opts.address}`,
|
||||
...(command ? [command] : []),
|
||||
]);
|
||||
await runRemoteCommand({ ...opts, cmd, username });
|
||||
}
|
||||
|
@ -174,14 +174,8 @@ export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
|
||||
);
|
||||
}
|
||||
|
||||
export interface ExtendedDockerOptions extends dockerode.DockerOptions {
|
||||
docker?: string; // socket path, e.g. /var/run/docker.sock
|
||||
dockerHost?: string; // host name or IP address
|
||||
dockerPort?: number; // TCP port number, e.g. 2375
|
||||
}
|
||||
|
||||
export async function getDocker(
|
||||
options: ExtendedDockerOptions,
|
||||
options: DockerConnectionCliFlags,
|
||||
): Promise<dockerode> {
|
||||
const connectOpts = await generateConnectOpts(options);
|
||||
const client = await createClient(connectOpts);
|
||||
@ -193,29 +187,21 @@ export async function createClient(
|
||||
opts: dockerode.DockerOptions,
|
||||
): Promise<dockerode> {
|
||||
const Docker = await import('dockerode');
|
||||
const docker = new Docker(opts);
|
||||
const { modem } = docker;
|
||||
// Workaround for a docker-modem 2.0.x bug where it sets a default
|
||||
// socketPath on Windows even if the input options specify a host/port.
|
||||
if (modem.socketPath && modem.host) {
|
||||
if (opts.socketPath) {
|
||||
modem.host = undefined;
|
||||
modem.port = undefined;
|
||||
} else if (opts.host) {
|
||||
modem.socketPath = undefined;
|
||||
}
|
||||
}
|
||||
return docker;
|
||||
return new Docker(opts);
|
||||
}
|
||||
|
||||
async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
||||
let connectOpts: dockerode.DockerOptions = {};
|
||||
|
||||
// Start with docker-modem defaults which take several env vars into account,
|
||||
// including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
|
||||
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
|
||||
const Modem = require('docker-modem');
|
||||
const defaultOpts = new Modem();
|
||||
/**
|
||||
* Initialize Docker connection options with the default values from the
|
||||
* 'docker-modem' package, which takes several env vars into account,
|
||||
* including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
|
||||
* https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
|
||||
*
|
||||
* @param opts Command line options like --dockerHost and --dockerPort
|
||||
*/
|
||||
export function getDefaultDockerModemOpts(
|
||||
opts: DockerConnectionCliFlags,
|
||||
): dockerode.DockerOptions {
|
||||
const connectOpts: dockerode.DockerOptions = {};
|
||||
const optsOfInterest: Array<keyof dockerode.DockerOptions> = [
|
||||
'ca',
|
||||
'cert',
|
||||
@ -227,9 +213,33 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
||||
'username',
|
||||
'timeout',
|
||||
];
|
||||
for (const opt of optsOfInterest) {
|
||||
connectOpts[opt] = defaultOpts[opt];
|
||||
const Modem = require('docker-modem');
|
||||
const originalDockerHost = process.env.DOCKER_HOST;
|
||||
try {
|
||||
if (opts.dockerHost) {
|
||||
process.env.DOCKER_HOST ||= opts.dockerPort
|
||||
? `${opts.dockerHost}:${opts.dockerPort}`
|
||||
: opts.dockerHost;
|
||||
}
|
||||
const defaultOpts = new Modem();
|
||||
for (const opt of optsOfInterest) {
|
||||
connectOpts[opt] = defaultOpts[opt];
|
||||
}
|
||||
} finally {
|
||||
// Did you know? Any value assigned to `process.env.XXX` becomes a string.
|
||||
// For example, `process.env.DOCKER_HOST = undefined` results in
|
||||
// value 'undefined' (a 9-character string) being assigned.
|
||||
if (originalDockerHost) {
|
||||
process.env.DOCKER_HOST = originalDockerHost;
|
||||
} else {
|
||||
delete process.env.DOCKER_HOST;
|
||||
}
|
||||
}
|
||||
return connectOpts;
|
||||
}
|
||||
|
||||
export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
|
||||
let connectOpts = getDefaultDockerModemOpts(opts);
|
||||
|
||||
// Now override the default options with any explicit command line options
|
||||
if (opts.docker != null && opts.dockerHost == null) {
|
||||
@ -253,9 +263,9 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
||||
// These should be file paths (strings)
|
||||
const tlsOpts = [opts.ca, opts.cert, opts.key];
|
||||
|
||||
// If any are set...
|
||||
// If any tlsOpts are set...
|
||||
if (tlsOpts.some((opt) => opt)) {
|
||||
// but not all ()
|
||||
// but not all
|
||||
if (!tlsOpts.every((opt) => opt)) {
|
||||
throw new ExpectedError(
|
||||
'You must provide a CA, certificate and key in order to use TLS',
|
||||
@ -270,7 +280,11 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
||||
const [ca, cert, key] = await Promise.all(
|
||||
tlsOpts.map((opt: string) => fs.readFile(opt, 'utf8')),
|
||||
);
|
||||
connectOpts = { ...connectOpts, ca, cert, key };
|
||||
// Also ensure that the protocol is 'https' like 'docker-modem' does:
|
||||
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L101-L103
|
||||
// TODO: delete redundant logic from this function now that similar logic
|
||||
// exists in the 'docker-modem' package.
|
||||
connectOpts = { ...connectOpts, ca, cert, key, protocol: 'https' };
|
||||
}
|
||||
|
||||
return connectOpts;
|
||||
|
@ -89,10 +89,10 @@ export async function sudo(
|
||||
await executeWithPrivileges(command, stderr, isCLIcmd);
|
||||
}
|
||||
|
||||
export function runCommand<T>(commandArgs: string[]): Promise<T> {
|
||||
export async function runCommand<T>(commandArgs: string[]): Promise<T> {
|
||||
const { isSubcommand } =
|
||||
require('../preparser') as typeof import('../preparser');
|
||||
if (isSubcommand(commandArgs)) {
|
||||
if (await isSubcommand(commandArgs)) {
|
||||
commandArgs = [
|
||||
commandArgs[0] + ':' + commandArgs[1],
|
||||
...commandArgs.slice(2),
|
||||
@ -151,26 +151,8 @@ export async function osProgressHandler(step: InitializeEmitter) {
|
||||
export async function getAppWithArch(
|
||||
applicationName: string,
|
||||
): Promise<ApplicationWithDeviceType & { arch: string }> {
|
||||
const app = await getApplication(applicationName);
|
||||
const { getExpanded } = await import('./pine');
|
||||
|
||||
return {
|
||||
...app,
|
||||
arch: getExpanded(
|
||||
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
|
||||
)!.slug,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Drop this. The sdk now has this baked in application.get().
|
||||
function getApplication(
|
||||
applicationName: string,
|
||||
): Promise<ApplicationWithDeviceType> {
|
||||
// Check for an app of the form `user/application`, and send
|
||||
// that off to a special handler (before importing any modules)
|
||||
const match = applicationName.split('/');
|
||||
|
||||
const extraOptions: BalenaSdk.PineOptions<BalenaSdk.Application> = {
|
||||
const { getApplication } = await import('./sdk');
|
||||
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'],
|
||||
@ -185,20 +167,20 @@ function getApplication(
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
if (match.length > 1) {
|
||||
return balena.models.application.getAppByOwner(
|
||||
match[1],
|
||||
match[0],
|
||||
extraOptions,
|
||||
) as Promise<ApplicationWithDeviceType>;
|
||||
}
|
||||
|
||||
return balena.models.application.get(
|
||||
const app = (await getApplication(
|
||||
balena,
|
||||
applicationName,
|
||||
extraOptions,
|
||||
) as Promise<ApplicationWithDeviceType>;
|
||||
options,
|
||||
)) as ApplicationWithDeviceType;
|
||||
const { getExpanded } = await import('./pine');
|
||||
|
||||
return {
|
||||
...app,
|
||||
arch: getExpanded(
|
||||
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
|
||||
)!.slug,
|
||||
};
|
||||
}
|
||||
|
||||
const second = 1000; // 1000 milliseconds
|
||||
@ -428,7 +410,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
|
||||
|
||||
export const expandForAppName = {
|
||||
$expand: {
|
||||
belongs_to__application: { $select: 'app_name' },
|
||||
belongs_to__application: { $select: ['app_name', 'slug'] as any },
|
||||
is_of__device_type: { $select: 'slug' },
|
||||
is_running__release: { $select: 'commit' },
|
||||
},
|
||||
|
@ -17,183 +17,11 @@
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs, Stats } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as MultiBuild from 'resin-multibuild';
|
||||
|
||||
import dockerIgnore = require('@zeit/dockerignore');
|
||||
import ignore from 'ignore';
|
||||
import type { Ignore } from '@balena/dockerignore';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
const { toPosixPath } = MultiBuild.PathUtils;
|
||||
|
||||
// v13: delete this enum
|
||||
export enum IgnoreFileType {
|
||||
DockerIgnore,
|
||||
GitIgnore,
|
||||
}
|
||||
|
||||
interface IgnoreEntry {
|
||||
pattern: string;
|
||||
// The relative file path from the base path of the build context
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used by the CLI v10 / v11 "original" tarDirectory function
|
||||
* in `compose.js`. It is still around for the benefit of the `--gitignore`
|
||||
* option, but is expected to be deleted in CLI v13.
|
||||
*
|
||||
* v13: delete this class
|
||||
*/
|
||||
export class FileIgnorer {
|
||||
private dockerIgnoreEntries: IgnoreEntry[];
|
||||
private gitIgnoreEntries: IgnoreEntry[];
|
||||
|
||||
private static ignoreFiles: Array<{
|
||||
pattern: string;
|
||||
type: IgnoreFileType;
|
||||
allowSubdirs: boolean;
|
||||
}> = [
|
||||
{
|
||||
pattern: '.gitignore',
|
||||
type: IgnoreFileType.GitIgnore,
|
||||
allowSubdirs: true,
|
||||
},
|
||||
{
|
||||
pattern: '.dockerignore',
|
||||
type: IgnoreFileType.DockerIgnore,
|
||||
allowSubdirs: false,
|
||||
},
|
||||
];
|
||||
|
||||
public constructor(public basePath: string) {
|
||||
this.dockerIgnoreEntries = [];
|
||||
this.gitIgnoreEntries = [];
|
||||
}
|
||||
/**
|
||||
* @param {string} relativePath
|
||||
* The relative pathname from the build context, for example a root level .gitignore should be
|
||||
* ./.gitignore
|
||||
* @returns IgnoreFileType
|
||||
* The type of ignore file, or null
|
||||
*/
|
||||
public getIgnoreFileType(relativePath: string): IgnoreFileType | null {
|
||||
for (const { pattern, type, allowSubdirs } of FileIgnorer.ignoreFiles) {
|
||||
if (
|
||||
path.basename(relativePath) === pattern &&
|
||||
(allowSubdirs || path.dirname(relativePath) === '.')
|
||||
) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* @param {string} fullPath
|
||||
* The full path on disk of the ignore file
|
||||
* @param {IgnoreFileType} type
|
||||
* @returns Promise
|
||||
*/
|
||||
public async addIgnoreFile(
|
||||
fullPath: string,
|
||||
type: IgnoreFileType,
|
||||
): Promise<void> {
|
||||
const contents = await fs.readFile(fullPath, 'utf8');
|
||||
|
||||
contents.split('\n').forEach((line) => {
|
||||
// ignore empty lines and comments
|
||||
if (/\s*#/.test(line) || _.isEmpty(line)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addEntry(line, fullPath, type);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass this function as a predicate to a filter function, and it will filter
|
||||
// any ignored files
|
||||
public filter = (filename: string): boolean => {
|
||||
const relFile = path.relative(this.basePath, filename);
|
||||
|
||||
// Don't ignore any metadata files
|
||||
// The regex below matches `.balena/qemu` and `myservice/.balena/qemu`
|
||||
// but not `some.dir.for.balena/qemu`.
|
||||
if (/(^|\/)\.(balena|resin)\//.test(toPosixPath(relFile))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't ignore Dockerfile (with or without extension) or docker-compose.yml
|
||||
if (
|
||||
/^Dockerfile$|^Dockerfile\.\S+/.test(path.basename(relFile)) ||
|
||||
path.basename(relFile) === 'docker-compose.yml'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const dockerIgnoreHandle = dockerIgnore();
|
||||
const gitIgnoreHandle = ignore();
|
||||
|
||||
interface IgnoreHandle {
|
||||
add: (pattern: string) => void;
|
||||
ignores: (file: string) => boolean;
|
||||
}
|
||||
|
||||
const ignoreTypes: Array<{
|
||||
handle: IgnoreHandle;
|
||||
entries: IgnoreEntry[];
|
||||
}> = [
|
||||
{ handle: dockerIgnoreHandle, entries: this.dockerIgnoreEntries },
|
||||
{ handle: gitIgnoreHandle, entries: this.gitIgnoreEntries },
|
||||
];
|
||||
|
||||
_.each(ignoreTypes, ({ handle, entries }) => {
|
||||
_.each(entries, ({ pattern, filePath }) => {
|
||||
if (FileIgnorer.contains(path.posix.dirname(filePath), filename)) {
|
||||
handle.add(pattern);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return !_.some(ignoreTypes, ({ handle }) => handle.ignores(relFile));
|
||||
}; // tslint:disable-line:semicolon
|
||||
|
||||
private addEntry(
|
||||
pattern: string,
|
||||
filePath: string,
|
||||
type: IgnoreFileType,
|
||||
): void {
|
||||
const entry: IgnoreEntry = { pattern, filePath };
|
||||
switch (type) {
|
||||
case IgnoreFileType.DockerIgnore:
|
||||
this.dockerIgnoreEntries.push(entry);
|
||||
break;
|
||||
case IgnoreFileType.GitIgnore:
|
||||
this.gitIgnoreEntries.push(entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two paths, check whether the first contains the second
|
||||
* @param path1 The potentially containing path
|
||||
* @param path2 The potentially contained path
|
||||
* @return A boolean indicating whether `path1` contains `path2`
|
||||
*/
|
||||
private static contains(path1: string, path2: string): boolean {
|
||||
// First normalise the input, to remove any path weirdness
|
||||
path1 = path.posix.normalize(path1);
|
||||
path2 = path.posix.normalize(path2);
|
||||
|
||||
// Now test if the start of the relative path contains ../ ,
|
||||
// which would tell us that path1 is not part of path2
|
||||
return !/^\.\.\//.test(path.posix.relative(path1, path2));
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileStats {
|
||||
filePath: string;
|
||||
relPath: string;
|
||||
@ -206,9 +34,7 @@ export interface FileStats {
|
||||
* @param projectDir Source directory (root of subtree to be listed)
|
||||
*/
|
||||
async function listFiles(projectDir: string): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
const files: string[] = [];
|
||||
dirs.push(projectDir);
|
||||
async function walk(currentDirs: string[]): Promise<string[]> {
|
||||
if (!currentDirs.length) {
|
||||
return files;
|
||||
@ -227,9 +53,12 @@ async function listFiles(projectDir: string): Promise<string[]> {
|
||||
const _files = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of _files) {
|
||||
const fpath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const isDirectory =
|
||||
entry.isDirectory() ||
|
||||
(entry.isSymbolicLink() && (await fs.stat(fpath)).isDirectory());
|
||||
|
||||
if (isDirectory) {
|
||||
foundDirs.push(fpath);
|
||||
dirs.push(fpath);
|
||||
} else {
|
||||
files.push(fpath);
|
||||
}
|
||||
@ -263,6 +92,7 @@ async function readDockerIgnoreFile(projectDir: string): Promise<string> {
|
||||
return dockerIgnoreStr;
|
||||
}
|
||||
|
||||
// TODO: docker-compose naming
|
||||
/**
|
||||
* Create an instance of '@balena/dockerignore', initialized with the contents
|
||||
* of a .dockerignore file (if any) found at the given directory argument, plus
|
||||
|
@ -21,7 +21,6 @@ import type { Chalk } from 'chalk';
|
||||
import type * as visuals from 'resin-cli-visuals';
|
||||
import type * as CliForm from 'resin-cli-form';
|
||||
import type { ux } from 'cli-ux';
|
||||
import type { stripIndent as StripIndent } from 'common-tags';
|
||||
|
||||
// Equivalent of _.once but avoiding the need to import lodash for lazy deps
|
||||
const once = <T>(fn: () => T) => {
|
||||
@ -63,4 +62,4 @@ export const getCliUx = once(() => require('cli-ux').ux as typeof ux);
|
||||
// Directly export stripIndent as we always use it immediately, but importing just `stripIndent` reduces startup time
|
||||
export const stripIndent =
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
require('common-tags/lib/stripIndent') as typeof StripIndent;
|
||||
require('common-tags/lib/stripIndent') as typeof import('common-tags/lib/stripIndent');
|
||||
|
@ -15,8 +15,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isV13 } from './version';
|
||||
|
||||
export const reachingOut = `\
|
||||
For further help or support, visit:
|
||||
https://www.balena.io/docs/reference/balena-cli/#support-faq-and-troubleshooting
|
||||
@ -88,60 +86,8 @@ If the --registry-secrets option is not specified, and a secrets.yml or
|
||||
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
||||
this file will be used instead.`;
|
||||
|
||||
const dockerignoreHelpV12 =
|
||||
'DOCKERIGNORE AND GITIGNORE FILES \n' +
|
||||
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
|
||||
the project root (--source directory) in order to decide which source files to
|
||||
exclude from the "build context" (tar stream) sent to balenaCloud, Docker
|
||||
daemon or balenaEngine. In a microservices (multicontainer) fleet, the
|
||||
source directory is the directory that contains the "docker-compose.yml" file.
|
||||
|
||||
The --multi-dockerignore (-m) option may be used with microservices
|
||||
(multicontainer) fleets that define a docker-compose.yml file. When this
|
||||
option is used, each service subdirectory (defined by the \`build\` or
|
||||
\`build.context\` service properties in the docker-compose.yml file) is
|
||||
filtered separately according to a .dockerignore file defined in the service
|
||||
subdirectory. If no .dockerignore file exists in a service subdirectory, then
|
||||
only the default .dockerignore patterns (see below) apply for that service
|
||||
subdirectory.
|
||||
|
||||
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
|
||||
any) defined at the overall project root will be used to filter files and
|
||||
subdirectories other than service subdirectories. It will not have any effect
|
||||
on service subdirectories, whether or not a service subdirectory defines its
|
||||
own .dockerignore file. Multiple .dockerignore files are not merged or added
|
||||
together, and cannot override or extend other files. This behavior maximizes
|
||||
compatibility with the standard docker-compose tool, while still allowing a
|
||||
root .dockerignore file (at the overall project root) to filter files and
|
||||
folders that are outside service subdirectories.
|
||||
|
||||
balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||
option if compatibility is required. This option is mutually exclusive with
|
||||
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||
release (v13).
|
||||
|
||||
Default .dockerignore patterns \n` +
|
||||
`When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
|
||||
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||
patterns found in the applicable .dockerignore files, in the following order:
|
||||
\`\`\`
|
||||
**/.git
|
||||
< user's patterns from the applicable '.dockerignore' file, if any >
|
||||
!**/.balena
|
||||
!**/.resin
|
||||
!**/Dockerfile
|
||||
!**/Dockerfile.*
|
||||
!**/docker-compose.yml
|
||||
\`\`\`
|
||||
These patterns always apply, whether or not .dockerignore files exist in the
|
||||
project. If necessary, the effect of the \`**/.git\` pattern may be modified by
|
||||
adding counter patterns to the applicable .dockerignore file(s), for example
|
||||
\`!mysubmodule/.git\`. For documentation on pattern format, see:
|
||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
- https://www.npmjs.com/package/@balena/dockerignore`;
|
||||
|
||||
const dockerignoreHelpV13 =
|
||||
// TODO: docker-compose naming
|
||||
export const dockerignoreHelp =
|
||||
'DOCKERIGNORE AND GITIGNORE FILES \n' +
|
||||
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
|
||||
the project root (--source directory) in order to decide which source files to
|
||||
@ -191,10 +137,6 @@ adding exception patterns to the applicable .dockerignore file(s), for example
|
||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
- https://www.npmjs.com/package/@balena/dockerignore`;
|
||||
|
||||
export const dockerignoreHelp = isV13()
|
||||
? dockerignoreHelpV13
|
||||
: dockerignoreHelpV12;
|
||||
|
||||
export const applicationIdInfo = `\
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
@ -218,6 +160,18 @@ created public/open fleet, or with fleets from other balena accounts that you
|
||||
may be invited to join under any role. For this reason, fleet names are
|
||||
especially discouraged in scripts (e.g. CI environments).`;
|
||||
|
||||
export const devModeInfo = `\
|
||||
The '--dev' option is used to configure balenaOS to operate in development mode,
|
||||
allowing anauthenticated root ssh access and exposing network ports such as
|
||||
balenaEngine's 2375 (unencrypted). This option causes \`"developmentMode": true\`
|
||||
to be inserted in the 'config.json' file in the image's boot partion. Development
|
||||
mode (as a configurable option) is applicable to balenaOS releases from early
|
||||
2022. Older releases have separate development and production balenaOS images
|
||||
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
|
||||
confuse the balenaOS "development mode" with a device's "local mode", the latter
|
||||
being a supervisor feature that allows the "balena push" command to push a user's
|
||||
application directly to a device in the local network.`;
|
||||
|
||||
export const jsonInfo = `\
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because field names are less likely to change in JSON format and because it
|
||||
@ -234,30 +188,6 @@ If you have a particular use for buildArg, which is not satisfied by build-time
|
||||
secrets, please contact us via support or the forums: https://forums.balena.io/
|
||||
\n`;
|
||||
|
||||
// Note: if you edit this message, check that the regex replace
|
||||
// logic in lib/commands/apps.ts still works.
|
||||
export const appToFleetCmdMsg = `\
|
||||
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
|
||||
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
|
||||
of the balena CLI (so that a different 'app' command can be implemented
|
||||
in the future). Use 'fleet' instead of 'app' to avoid this warning.
|
||||
Find out more at: https://git.io/JRuZr`;
|
||||
|
||||
export const appToFleetFlagMsg = `\
|
||||
Renaming notice: The '-a', '--app' or '--application' options are now
|
||||
aliases for the '-f' or '--fleet' options. THE ALIASES WILL BE REMOVED
|
||||
in the next major version of the balena CLI (so that a different '--app'
|
||||
option can be implemented in the future). Use '-f' or '--fleet' instead.
|
||||
Find out more at: https://git.io/JRuZr`;
|
||||
|
||||
export const appToFleetOutputMsg = `\
|
||||
Renaming notice: The 'app' or 'application' words in table headers
|
||||
or in JSON object keys/properties will be replaced with 'fleet' in
|
||||
the next major version of the CLI (v13). The --v13 option may be used
|
||||
to enable the new names already now, and suppress a warning message.
|
||||
(The --v13 option will be silently ignored in CLI v13.)
|
||||
Find out more at: https://git.io/JRuZr`;
|
||||
|
||||
export function getNodeEngineVersionWarn(
|
||||
version: string,
|
||||
validVersions: string,
|
||||
|
@ -13,15 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
|
||||
import type { Application, BalenaSDK, Device, Organization } from 'balena-sdk';
|
||||
import _ = require('lodash');
|
||||
|
||||
import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
|
||||
import validation = require('./validation');
|
||||
import { delay } from './helpers';
|
||||
import { isV13 } from './version';
|
||||
import type { Application, Device, Organization } from 'balena-sdk';
|
||||
|
||||
export function authenticate(options: {}): Promise<void> {
|
||||
const balena = getBalenaSdk();
|
||||
@ -142,11 +141,7 @@ export async function confirm(
|
||||
) {
|
||||
if (yesOption) {
|
||||
if (yesMessage) {
|
||||
if (isV13()) {
|
||||
console.error(yesMessage);
|
||||
} else {
|
||||
console.log(yesMessage);
|
||||
}
|
||||
console.log(yesMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -174,7 +169,7 @@ export function selectApplication(
|
||||
throw new ExpectedError('No fleets found');
|
||||
}
|
||||
|
||||
const apps = (await balena.models.application.getAll({
|
||||
const apps = (await balena.models.application.getAllDirectlyAccessible({
|
||||
$expand: {
|
||||
is_for__device_type: {
|
||||
$select: 'slug',
|
||||
@ -212,35 +207,6 @@ export async function selectOrganization(organizations?: Organization[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function awaitDevice(uuid: string) {
|
||||
const balena = getBalenaSdk();
|
||||
const deviceName = await balena.models.device.getName(uuid);
|
||||
const visuals = getVisuals();
|
||||
const spinner = new visuals.Spinner(
|
||||
`Waiting for ${deviceName} to come online`,
|
||||
);
|
||||
|
||||
const poll = async (): Promise<void> => {
|
||||
const isOnline = await balena.models.device.isOnline(uuid);
|
||||
if (isOnline) {
|
||||
spinner.stop();
|
||||
console.info(`The device **${deviceName}** is online!`);
|
||||
return;
|
||||
} else {
|
||||
// Spinner implementation is smart enough to
|
||||
// not start again if it was already started
|
||||
spinner.start();
|
||||
|
||||
await delay(3000);
|
||||
await poll();
|
||||
}
|
||||
};
|
||||
|
||||
console.info(`Waiting for ${deviceName} to connect to balena...`);
|
||||
await poll();
|
||||
return uuid;
|
||||
}
|
||||
|
||||
export async function awaitDeviceOsUpdate(
|
||||
uuid: string,
|
||||
targetOsVersion: string,
|
||||
@ -326,7 +292,7 @@ export function inferOrSelectDevice(preferredUuid: string) {
|
||||
* TODO: Modify this when app IDs dropped.
|
||||
*/
|
||||
export async function getOnlineTargetDeviceUuid(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
sdk: BalenaSDK,
|
||||
applicationOrDevice: string,
|
||||
) {
|
||||
const logger = (await import('../utils/logger')).getLogger();
|
||||
|
@ -20,7 +20,7 @@ import { ExpectedError, printErrorMessage } from '../errors';
|
||||
import { getVisuals, stripIndent, getCliForm } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
import { confirm } from './patterns';
|
||||
import { exec, execBuffered, getDeviceOsRelease } from './ssh';
|
||||
import { getLocalDeviceCmdStdout, getDeviceOsRelease } from './ssh';
|
||||
|
||||
const MIN_BALENAOS_VERSION = 'v2.14.0';
|
||||
|
||||
@ -80,7 +80,12 @@ export async function leave(
|
||||
logger.logDebug('Deconfiguring...');
|
||||
await deconfigure(deviceHostnameOrIp);
|
||||
|
||||
logger.logSuccess('Device successfully left the platform.');
|
||||
logger.logSuccess(stripIndent`
|
||||
Device successfully left the platform. The device will still be listed as part
|
||||
of the fleet, but changes to the fleet will no longer affect the device and its
|
||||
status will eventually be reported as 'Offline'. To irrecoverably delete the
|
||||
device from the fleet, use the 'balena device rm' command or delete it through
|
||||
the balenaCloud web dashboard.`);
|
||||
}
|
||||
|
||||
async function execCommand(
|
||||
@ -88,20 +93,25 @@ async function execCommand(
|
||||
cmd: string,
|
||||
msg: string,
|
||||
): Promise<void> {
|
||||
const through = await import('through2');
|
||||
const { Writable } = await import('stream');
|
||||
const visuals = getVisuals();
|
||||
|
||||
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
|
||||
const innerSpinner = spinner.spinner;
|
||||
|
||||
const stream = through(function (data, _enc, cb) {
|
||||
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
|
||||
cb(null, data);
|
||||
const stream = new Writable({
|
||||
write(_chunk: Buffer, _enc, callback) {
|
||||
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
|
||||
spinner.start();
|
||||
await exec(deviceIp, cmd, stream);
|
||||
spinner.stop();
|
||||
try {
|
||||
await getLocalDeviceCmdStdout(deviceIp, cmd, stream);
|
||||
} finally {
|
||||
spinner.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async function configure(deviceIp: string, config: any): Promise<void> {
|
||||
@ -121,7 +131,7 @@ async function deconfigure(deviceIp: string): Promise<void> {
|
||||
async function assertDeviceIsCompatible(deviceIp: string): Promise<void> {
|
||||
const cmd = 'os-config --version';
|
||||
try {
|
||||
await execBuffered(deviceIp, cmd);
|
||||
await getLocalDeviceCmdStdout(deviceIp, cmd);
|
||||
} catch (err) {
|
||||
if (err instanceof ExpectedError) {
|
||||
throw err;
|
||||
@ -235,7 +245,7 @@ async function getOrSelectApplication(
|
||||
name = appName;
|
||||
}
|
||||
|
||||
const applications = (await sdk.models.application.getAll(
|
||||
const applications = (await sdk.models.application.getAllDirectlyAccessible(
|
||||
options,
|
||||
)) as ApplicationWithDeviceType[];
|
||||
|
||||
@ -273,7 +283,7 @@ async function createOrSelectApp(
|
||||
deviceType: string,
|
||||
): Promise<ApplicationWithDeviceType> {
|
||||
// No fleet specified, show a list to select one.
|
||||
const applications = (await sdk.models.application.getAll({
|
||||
const applications = (await sdk.models.application.getAllDirectlyAccessible({
|
||||
$expand: { is_for__device_type: { $select: 'slug' } },
|
||||
$filter: {
|
||||
is_for__device_type: {
|
||||
@ -322,10 +332,13 @@ async function createApplication(
|
||||
});
|
||||
|
||||
try {
|
||||
await sdk.models.application.get(appName, {
|
||||
await sdk.models.application.getDirectlyAccessible(appName, {
|
||||
$filter: {
|
||||
$or: [
|
||||
{ slug: { $startswith: `${username!.toLowerCase()}/` } },
|
||||
// TODO: do we still need the following filter? Is it for
|
||||
// old openBalena instances where slugs were equal to the
|
||||
// app name and did not contain the slash character?
|
||||
{ $not: { slug: { $contains: '/' } } },
|
||||
],
|
||||
},
|
||||
|
@ -50,7 +50,6 @@ export interface RemoteBuild {
|
||||
source: string;
|
||||
auth: string;
|
||||
baseUrl: string;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
opts: BuildOpts;
|
||||
sdk: BalenaSDK;
|
||||
// For internal use
|
||||
@ -213,7 +212,7 @@ function handleBuilderMetadata(obj: BuilderMessage, build: RemoteBuild) {
|
||||
}
|
||||
|
||||
const value = match[1];
|
||||
const amount = match[2] || 1;
|
||||
const amount = Number(match[2]) || 1;
|
||||
|
||||
switch (value) {
|
||||
case 'erase':
|
||||
@ -323,7 +322,6 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
|
||||
preFinalizeCallback: preFinalizeCb,
|
||||
convertEol: build.opts.convertEol,
|
||||
multiDockerignore: build.opts.multiDockerignore,
|
||||
nogitignore: build.nogitignore, // v13: delete this line
|
||||
});
|
||||
globalLogger.logDebug(
|
||||
`Tarring complete in ${Date.now() - tarStartTime} ms`,
|
||||
@ -406,7 +404,7 @@ async function getRemoteBuildStream(
|
||||
if (process.stdout.isTTY) {
|
||||
const visuals = getVisuals();
|
||||
uploadSpinner = new visuals.Spinner(
|
||||
'Uploading source package to balenaCloud',
|
||||
`Uploading source package to ${new URL(builderUrl).origin}`,
|
||||
);
|
||||
(uploadSpinner as any).start();
|
||||
}
|
||||
|
@ -23,21 +23,36 @@ import type {
|
||||
} from 'balena-sdk';
|
||||
|
||||
/**
|
||||
* Wraps the sdk application.get method,
|
||||
* adding disambiguation in cases where the provided
|
||||
* identifier could be interpreted in multiple valid ways.
|
||||
* Get a fleet object, disambiguating the fleet identifier which may be a
|
||||
* a fleet slug, name or numeric database ID (as a string).
|
||||
* TODO: add support for fleet UUIDs.
|
||||
*/
|
||||
export async function getApplication(
|
||||
sdk: BalenaSDK,
|
||||
nameOrSlugOrId: string | number,
|
||||
options?: PineOptions<Application>,
|
||||
): Promise<Application> {
|
||||
const { looksLikeInteger } = await import('./validation');
|
||||
if (looksLikeInteger(nameOrSlugOrId as string)) {
|
||||
const { looksLikeFleetSlug, looksLikeInteger } = await import('./validation');
|
||||
if (
|
||||
typeof nameOrSlugOrId === 'string' &&
|
||||
looksLikeFleetSlug(nameOrSlugOrId)
|
||||
) {
|
||||
return await sdk.models.application.getDirectlyAccessible(
|
||||
nameOrSlugOrId,
|
||||
options,
|
||||
);
|
||||
}
|
||||
if (typeof nameOrSlugOrId === 'number' || looksLikeInteger(nameOrSlugOrId)) {
|
||||
try {
|
||||
// Test for existence of app with this numerical ID
|
||||
return await sdk.models.application.get(Number(nameOrSlugOrId), options);
|
||||
return await sdk.models.application.getDirectlyAccessible(
|
||||
Number(nameOrSlugOrId),
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
if (typeof nameOrSlugOrId === 'number') {
|
||||
throw e;
|
||||
}
|
||||
const { instanceOf } = await import('../errors');
|
||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||
if (!instanceOf(e, BalenaApplicationNotFound)) {
|
||||
@ -46,47 +61,34 @@ export async function getApplication(
|
||||
// App with this numerical ID not found, but there may be an app with this numerical name.
|
||||
}
|
||||
}
|
||||
return sdk.models.application.get(nameOrSlugOrId, options);
|
||||
// Not a slug and not a numeric database ID: must be an app name.
|
||||
// TODO: revisit this logic when we add support for fleet UUIDs.
|
||||
return await sdk.models.application.getAppByName(
|
||||
nameOrSlugOrId,
|
||||
options,
|
||||
'directly_accessible',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an string representation of an application identifier,
|
||||
* which could be one of:
|
||||
* - name (including numeric names)
|
||||
* - slug
|
||||
* - numerical id
|
||||
* disambiguate and return a properly typed identifier.
|
||||
*
|
||||
* Attempts to minimise the number of API calls required.
|
||||
* TODO: Remove this once support for numeric App IDs is removed.
|
||||
* Given a fleet name, slug or numeric database ID, return its slug.
|
||||
* This function conditionally makes an async SDK/API call to retrieve the
|
||||
* application object, which can be wasteful if the application object is
|
||||
* required before or after the call to this function. If this is the case,
|
||||
* consider calling `getApplication()` and reusing the application object.
|
||||
*/
|
||||
export async function getTypedApplicationIdentifier(
|
||||
export async function getFleetSlug(
|
||||
sdk: BalenaSDK,
|
||||
nameOrSlugOrId: string,
|
||||
) {
|
||||
const { looksLikeInteger } = await import('./validation');
|
||||
// If there's no possible ambiguity,
|
||||
// return the passed identifier unchanged
|
||||
if (!looksLikeInteger(nameOrSlugOrId)) {
|
||||
return nameOrSlugOrId;
|
||||
nameOrSlugOrId: string | number,
|
||||
): Promise<string> {
|
||||
const { looksLikeFleetSlug } = await import('./validation');
|
||||
if (
|
||||
typeof nameOrSlugOrId === 'string' &&
|
||||
looksLikeFleetSlug(nameOrSlugOrId)
|
||||
) {
|
||||
return nameOrSlugOrId.toLowerCase();
|
||||
}
|
||||
|
||||
// Resolve ambiguity
|
||||
try {
|
||||
// Test for existence of app with this numerical ID,
|
||||
// and return typed id if found
|
||||
return (await sdk.models.application.get(Number(nameOrSlugOrId))).id;
|
||||
} catch (e) {
|
||||
const { instanceOf } = await import('../errors');
|
||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||
if (!instanceOf(e, BalenaApplicationNotFound)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// App with this numerical id not found
|
||||
// return the passed identifier unchanged
|
||||
return nameOrSlugOrId;
|
||||
return (await getApplication(sdk, nameOrSlugOrId)).slug;
|
||||
}
|
||||
|
||||
/**
|
||||
|
377
lib/utils/ssh.ts
377
lib/utils/ssh.ts
@ -16,147 +16,314 @@
|
||||
*/
|
||||
import { spawn, StdioOptions } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import { TypedError } from 'typed-error';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
export class ExecError extends TypedError {
|
||||
public cmd: string;
|
||||
public exitCode: number;
|
||||
export class SshPermissionDeniedError extends ExpectedError {}
|
||||
|
||||
constructor(cmd: string, exitCode: number) {
|
||||
super(`Command '${cmd}' failed with error: ${exitCode}`);
|
||||
export class RemoteCommandError extends ExpectedError {
|
||||
cmd: string;
|
||||
exitCode?: number;
|
||||
exitSignal?: NodeJS.Signals;
|
||||
|
||||
constructor(cmd: string, exitCode?: number, exitSignal?: NodeJS.Signals) {
|
||||
super(sshErrorMessage(cmd, exitSignal, exitCode));
|
||||
this.cmd = cmd;
|
||||
this.exitCode = exitCode;
|
||||
this.exitSignal = exitSignal;
|
||||
}
|
||||
}
|
||||
|
||||
export async function exec(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
stdout?: NodeJS.WritableStream,
|
||||
): Promise<void> {
|
||||
export interface SshRemoteCommandOpts {
|
||||
cmd?: string;
|
||||
hostname: string;
|
||||
ignoreStdin?: boolean;
|
||||
port?: number | 'cloud' | 'local';
|
||||
proxyCommand?: string[];
|
||||
username?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export const stdioIgnore: {
|
||||
stdin: 'ignore';
|
||||
stdout: 'ignore';
|
||||
stderr: 'ignore';
|
||||
} = {
|
||||
stdin: 'ignore',
|
||||
stdout: 'ignore',
|
||||
stderr: 'ignore',
|
||||
};
|
||||
|
||||
export function sshArgsForRemoteCommand({
|
||||
cmd = '',
|
||||
hostname,
|
||||
ignoreStdin = false,
|
||||
port,
|
||||
proxyCommand,
|
||||
username = 'root',
|
||||
verbose = false,
|
||||
}: SshRemoteCommandOpts): string[] {
|
||||
port = port === 'local' ? 22222 : port === 'cloud' ? 22 : port;
|
||||
return [
|
||||
...(verbose ? ['-vvv'] : []),
|
||||
...(ignoreStdin ? ['-n'] : []),
|
||||
'-t',
|
||||
...(port ? ['-p', port.toString()] : []),
|
||||
...['-o', 'LogLevel=ERROR'],
|
||||
...['-o', 'StrictHostKeyChecking=no'],
|
||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||
...(proxyCommand && proxyCommand.length
|
||||
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
|
||||
: []),
|
||||
`${username}@${hostname}`,
|
||||
...(cmd ? [cmd] : []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given command on a local balenaOS device over ssh.
|
||||
* @param cmd Shell command to execute on the device
|
||||
* @param hostname Device's hostname or IP address
|
||||
* @param port SSH server TCP port number or 'local' (22222) or 'cloud' (22)
|
||||
* @param stdin Readable stream to pipe to the remote command stdin,
|
||||
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
|
||||
* @param stdout Writeable stream to pipe from the remote command stdout,
|
||||
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
|
||||
* @param stderr Writeable stream to pipe from the remote command stdout,
|
||||
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
|
||||
* @param username SSH username for authorization. With balenaOS 2.44.0 or
|
||||
* later, it can be a balenaCloud username.
|
||||
* @param verbose Produce debugging output
|
||||
*/
|
||||
export async function runRemoteCommand({
|
||||
cmd = '',
|
||||
hostname,
|
||||
port,
|
||||
proxyCommand,
|
||||
stdin = 'inherit',
|
||||
stdout = 'inherit',
|
||||
stderr = 'inherit',
|
||||
username = 'root',
|
||||
verbose = false,
|
||||
}: SshRemoteCommandOpts & {
|
||||
stdin?: 'ignore' | 'inherit' | NodeJS.ReadableStream;
|
||||
stdout?: 'ignore' | 'inherit' | NodeJS.WritableStream;
|
||||
stderr?: 'ignore' | 'inherit' | NodeJS.WritableStream;
|
||||
}): Promise<void> {
|
||||
let ignoreStdin: boolean;
|
||||
if (stdin === 'ignore') {
|
||||
// Set ignoreStdin=true in order for the "ssh -n" option to be used to
|
||||
// prevent the ssh client from using the CLI process stdin. In addition,
|
||||
// stdin must be forced to 'inherit' (if it is not a readable stream) in
|
||||
// order to work around a bug in older versions of the built-in Windows
|
||||
// 10 ssh client that otherwise prints the following to stderr and
|
||||
// hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
|
||||
// They actually fixed the bug in newer versions of the ssh client:
|
||||
// https://github.com/PowerShell/Win32-OpenSSH/issues/856 but users
|
||||
// have to manually download and install a new client.
|
||||
ignoreStdin = true;
|
||||
stdin = 'inherit';
|
||||
} else {
|
||||
ignoreStdin = false;
|
||||
}
|
||||
const { which } = await import('./which');
|
||||
const program = await which('ssh');
|
||||
const args = [
|
||||
'-n',
|
||||
'-t',
|
||||
'-p',
|
||||
'22222',
|
||||
'-o',
|
||||
'LogLevel=ERROR',
|
||||
'-o',
|
||||
'StrictHostKeyChecking=no',
|
||||
'-o',
|
||||
'UserKnownHostsFile=/dev/null',
|
||||
`root@${deviceIp}`,
|
||||
const args = sshArgsForRemoteCommand({
|
||||
cmd,
|
||||
];
|
||||
hostname,
|
||||
ignoreStdin,
|
||||
port,
|
||||
proxyCommand,
|
||||
username,
|
||||
verbose,
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
const logger = (await import('./logger')).getLogger();
|
||||
logger.logDebug(`Executing [${program},${args}]`);
|
||||
}
|
||||
|
||||
// Note: stdin must be 'inherit' to workaround a bug in older versions of
|
||||
// the built-in Windows 10 ssh client that otherwise prints the following
|
||||
// to stderr and hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
|
||||
// They fixed the bug in newer versions of the ssh client:
|
||||
// https://github.com/PowerShell/Win32-OpenSSH/issues/856
|
||||
// but users whould have to manually download and install a new client.
|
||||
// Note that "ssh -n" does not solve the problem, but should in theory
|
||||
// prevent the ssh client from using the CLI process stdin, even if it
|
||||
// is connected with 'inherit'.
|
||||
const stdio: StdioOptions = [
|
||||
'inherit',
|
||||
stdout ? 'pipe' : 'inherit',
|
||||
'inherit',
|
||||
typeof stdin === 'string' ? stdin : 'pipe',
|
||||
typeof stdout === 'string' ? stdout : 'pipe',
|
||||
typeof stderr === 'string' ? stderr : 'pipe',
|
||||
];
|
||||
let exitCode: number | undefined;
|
||||
let exitSignal: NodeJS.Signals | undefined;
|
||||
try {
|
||||
[exitCode, exitSignal] = await new Promise<[number, NodeJS.Signals]>(
|
||||
(resolve, reject) => {
|
||||
const ps = spawn(program, args, { stdio })
|
||||
.on('error', reject)
|
||||
.on('close', (code, signal) => resolve([code, signal]));
|
||||
|
||||
const exitCode = await new Promise<number>((resolve, reject) => {
|
||||
const ps = spawn(program, args, { stdio })
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
|
||||
if (stdout) {
|
||||
ps.stdout.pipe(stdout);
|
||||
}
|
||||
});
|
||||
if (exitCode !== 0) {
|
||||
throw new ExecError(cmd, exitCode);
|
||||
if (ps.stdin && stdin && typeof stdin !== 'string') {
|
||||
stdin.pipe(ps.stdin);
|
||||
}
|
||||
if (ps.stdout && stdout && typeof stdout !== 'string') {
|
||||
ps.stdout.pipe(stdout);
|
||||
}
|
||||
if (ps.stderr && stderr && typeof stderr !== 'string') {
|
||||
ps.stderr.pipe(stderr);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
const msg = [
|
||||
`ssh failed with exit code=${exitCode} signal=${exitSignal}:`,
|
||||
`[${program}, ${args.join(', ')}]`,
|
||||
...(error ? [`${error}`] : []),
|
||||
];
|
||||
throw new ExpectedError(msg.join('\n'));
|
||||
}
|
||||
if (exitCode || exitSignal) {
|
||||
throw new RemoteCommandError(cmd, exitCode, exitSignal);
|
||||
}
|
||||
}
|
||||
|
||||
export async function execBuffered(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
enc?: string,
|
||||
): Promise<string> {
|
||||
const through = await import('through2');
|
||||
const buffer: string[] = [];
|
||||
await exec(
|
||||
deviceIp,
|
||||
/**
|
||||
* Execute the given command on a local balenaOS device over ssh.
|
||||
* Capture stdout and/or stderr to Buffers and return them.
|
||||
*
|
||||
* @param deviceIp IP address of the local device
|
||||
* @param cmd Shell command to execute on the device
|
||||
* @param opts Options
|
||||
* @param opts.username SSH username for authorization. With balenaOS 2.44.0 or
|
||||
* later, it may be a balenaCloud username. Otherwise, 'root'.
|
||||
* @param opts.stdin Passed through to the runRemoteCommand function
|
||||
* @param opts.stdout If 'capture', capture stdout to a Buffer.
|
||||
* @param opts.stderr If 'capture', capture stdout to a Buffer.
|
||||
*/
|
||||
export async function getRemoteCommandOutput({
|
||||
cmd,
|
||||
hostname,
|
||||
port,
|
||||
proxyCommand,
|
||||
stdin = 'ignore',
|
||||
stdout = 'capture',
|
||||
stderr = 'capture',
|
||||
username = 'root',
|
||||
verbose = false,
|
||||
}: SshRemoteCommandOpts & {
|
||||
stdin?: 'ignore' | 'inherit' | NodeJS.ReadableStream;
|
||||
stdout?: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream;
|
||||
stderr?: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream;
|
||||
}): Promise<{ stdout: Buffer; stderr: Buffer }> {
|
||||
const { Writable } = await import('stream');
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
const stderrChunks: Buffer[] = [];
|
||||
const stdoutStream = new Writable({
|
||||
write(chunk: Buffer, _enc, callback) {
|
||||
stdoutChunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const stderrStream = new Writable({
|
||||
write(chunk: Buffer, _enc, callback) {
|
||||
stderrChunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
await runRemoteCommand({
|
||||
cmd,
|
||||
through(function (data, _enc, cb) {
|
||||
buffer.push(data.toString(enc));
|
||||
cb();
|
||||
}),
|
||||
);
|
||||
return buffer.join('');
|
||||
hostname,
|
||||
port,
|
||||
proxyCommand,
|
||||
stdin,
|
||||
stdout: stdout === 'capture' ? stdoutStream : stdout,
|
||||
stderr: stderr === 'capture' ? stderrStream : stderr,
|
||||
username,
|
||||
verbose,
|
||||
});
|
||||
return {
|
||||
stdout: Buffer.concat(stdoutChunks),
|
||||
stderr: Buffer.concat(stderrChunks),
|
||||
};
|
||||
}
|
||||
|
||||
/** Convenience wrapper for getRemoteCommandOutput */
|
||||
export async function getLocalDeviceCmdStdout(
|
||||
hostname: string,
|
||||
cmd: string,
|
||||
stdout: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream = 'capture',
|
||||
): Promise<Buffer> {
|
||||
const port = 'local';
|
||||
return (
|
||||
await getRemoteCommandOutput({
|
||||
cmd,
|
||||
hostname,
|
||||
port,
|
||||
stdout,
|
||||
stderr: 'inherit',
|
||||
username: await findBestUsernameForDevice(hostname, port),
|
||||
})
|
||||
).stdout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a trivial 'exit 0' command over ssh on the target hostname (typically the
|
||||
* IP address of a local device) with the 'root' username, in order to determine
|
||||
* whether root authentication suceeds. It should succeed with development
|
||||
* variants of balenaOS and fail with production variants, unless a ssh key was
|
||||
* added to the device's 'config.json' file.
|
||||
* @return True if succesful, false on any errors.
|
||||
*/
|
||||
export const isRootUserGood = _.memoize(async (hostname: string, port) => {
|
||||
try {
|
||||
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...stdioIgnore });
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Determine whether the given local device (hostname or IP address) should be
|
||||
* accessed as the 'root' user or as a regular cloud user (balenaCloud or
|
||||
* openBalena). Where possible, the root user is preferable because:
|
||||
* - It allows ssh to be used in air-gapped scenarios (no internet access).
|
||||
* Logging in as a regular user requires the device to fetch public keys from
|
||||
* the cloud backend.
|
||||
* - Root authentication is significantly faster for local devices (a fraction
|
||||
* of a second versus 5+ seconds).
|
||||
* - Non-root authentication requires balenaOS v2.44.0 or later, so not (yet)
|
||||
* universally possible.
|
||||
*/
|
||||
export const findBestUsernameForDevice = _.memoize(
|
||||
async (hostname: string, port): Promise<string> => {
|
||||
let username: string | undefined;
|
||||
if (await isRootUserGood(hostname, port)) {
|
||||
username = 'root';
|
||||
} else {
|
||||
const { getCachedUsername } = await import('./bootstrap');
|
||||
username = (await getCachedUsername())?.username;
|
||||
}
|
||||
if (!username) {
|
||||
const { stripIndent } = await import('./lazy');
|
||||
throw new ExpectedError(stripIndent`
|
||||
SSH authentication failed for 'root@${hostname}'.
|
||||
Please login with 'balena login' for alternative authentication.`);
|
||||
}
|
||||
return username;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Return a device's balenaOS release by executing 'cat /etc/os-release'
|
||||
* over ssh to the given deviceIp address. The result is cached with
|
||||
* lodash's memoize.
|
||||
*/
|
||||
export const getDeviceOsRelease = _.memoize(async (deviceIp: string) =>
|
||||
execBuffered(deviceIp, 'cat /etc/os-release'),
|
||||
export const getDeviceOsRelease = _.memoize(async (hostname: string) =>
|
||||
(await getLocalDeviceCmdStdout(hostname, 'cat /etc/os-release')).toString(),
|
||||
);
|
||||
|
||||
// TODO: consolidate the various forms of executing ssh child processes
|
||||
// in the CLI, like exec and spawn, starting with the files:
|
||||
// lib/actions/ssh.ts
|
||||
// lib/utils/ssh.ts
|
||||
// lib/utils/device/ssh.ts
|
||||
|
||||
/**
|
||||
* Obtain the full path for ssh using which, then spawn a child process.
|
||||
* - If the child process returns error code 0, return the function normally
|
||||
* (do not throw an error).
|
||||
* - If the child process returns a non-zero error code, set process.exitCode
|
||||
* to that error code, and throw ExpectedError with a warning message.
|
||||
* - If the child process is terminated by a process signal, set
|
||||
* process.exitCode = 1, and throw ExpectedError with a warning message.
|
||||
*/
|
||||
export async function spawnSshAndThrowOnError(
|
||||
args: string[],
|
||||
options?: import('child_process').SpawnOptions,
|
||||
) {
|
||||
const { whichSpawn } = await import('./which');
|
||||
const [exitCode, exitSignal] = await whichSpawn(
|
||||
'ssh',
|
||||
args,
|
||||
options,
|
||||
true, // returnExitCodeOrSignal
|
||||
);
|
||||
if (exitCode || exitSignal) {
|
||||
// ssh returns a wide range of exit codes, including return codes of
|
||||
// interactive shells. For example, if the user types CTRL-C on an
|
||||
// interactive shell and then `exit`, ssh returns error code 130.
|
||||
// Another example, typing "exit 1" on an interactive shell causes ssh
|
||||
// to return exit code 1. In these cases, print a short one-line warning
|
||||
// message, and exits the CLI process with the same error code.
|
||||
process.exitCode = exitCode;
|
||||
throw new ExpectedError(sshErrorMessage(exitSignal, exitCode));
|
||||
}
|
||||
}
|
||||
|
||||
function sshErrorMessage(exitSignal?: string, exitCode?: number) {
|
||||
function sshErrorMessage(cmd: string, exitSignal?: string, exitCode?: number) {
|
||||
const msg: string[] = [];
|
||||
cmd = cmd ? `Remote command "${cmd}"` : 'Process';
|
||||
if (exitSignal) {
|
||||
msg.push(`Warning: ssh process was terminated with signal "${exitSignal}"`);
|
||||
msg.push(`SSH: ${cmd} terminated with signal "${exitSignal}"`);
|
||||
} else {
|
||||
msg.push(`Warning: ssh process exited with non-zero code "${exitCode}"`);
|
||||
msg.push(`SSH: ${cmd} exited with non-zero status code "${exitCode}"`);
|
||||
switch (exitCode) {
|
||||
case 255:
|
||||
msg.push(`
|
||||
|
@ -98,7 +98,7 @@ async function spawnAndPipe(
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
if (stderr) {
|
||||
if (stderr && ps.stderr) {
|
||||
ps.stderr.pipe(stderr);
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2019 Balena
|
||||
Copyright 2016-2022 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -40,14 +40,35 @@ export function notify() {
|
||||
}
|
||||
}
|
||||
const up = notifier.update;
|
||||
if (
|
||||
up &&
|
||||
(require('semver') as typeof import('semver')).lt(up.current, up.latest)
|
||||
) {
|
||||
notifier.notify({
|
||||
defer: false,
|
||||
message: `Update available ${up.current} → ${up.latest}\n
|
||||
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md`,
|
||||
});
|
||||
const message = up && getNotifierMessage(up);
|
||||
if (message) {
|
||||
notifier.notify({ defer: false, message });
|
||||
}
|
||||
}
|
||||
|
||||
export function getNotifierMessage(updateInfo: UpdateNotifier.UpdateInfo) {
|
||||
const semver = require('semver') as typeof import('semver');
|
||||
const message: string[] = [];
|
||||
const [current, latest] = [updateInfo.current, updateInfo.latest];
|
||||
|
||||
if (semver.lt(current, latest)) {
|
||||
message.push(
|
||||
`Update available ${current} → ${latest}`,
|
||||
'https://github.com/balena-io/balena-cli/blob/master/INSTALL.md',
|
||||
);
|
||||
const currentMajor = semver.major(current);
|
||||
const latestMajor = semver.major(latest);
|
||||
if (currentMajor !== latestMajor) {
|
||||
message.push(
|
||||
'',
|
||||
`Check the v${latestMajor} release notes at:`,
|
||||
getReleaseNotesUrl(latestMajor),
|
||||
);
|
||||
}
|
||||
}
|
||||
return message.join('\n');
|
||||
}
|
||||
|
||||
function getReleaseNotesUrl(majorVersion: number) {
|
||||
return `https://github.com/balena-io/balena-cli/wiki/CLI-v${majorVersion}-Release-Notes`;
|
||||
}
|
||||
|
@ -118,7 +118,6 @@ export function parseAsLocalHostnameOrIp(input: string, paramName?: string) {
|
||||
return input;
|
||||
}
|
||||
|
||||
export function looksLikeAppSlug(input: string) {
|
||||
// One or more non whitespace chars, /, 4 or more non whitespace chars
|
||||
return /[\S]+\/[\S]{4,}/.test(input);
|
||||
export function looksLikeFleetSlug(input: string) {
|
||||
return input.includes('/');
|
||||
}
|
||||
|
@ -22,12 +22,12 @@ export function isVersionGTE(v: string): boolean {
|
||||
return semver.gte(process.env.BALENA_CLI_VERSION_OVERRIDE || version, v);
|
||||
}
|
||||
|
||||
let v13: boolean;
|
||||
let v14: boolean;
|
||||
|
||||
/** Feature switch for the next major version of the CLI */
|
||||
export function isV13(): boolean {
|
||||
if (v13 === undefined) {
|
||||
v13 = isVersionGTE('13.0.0');
|
||||
export function isV14(): boolean {
|
||||
if (v14 === undefined) {
|
||||
v14 = isVersionGTE('14.0.0');
|
||||
}
|
||||
return v13;
|
||||
return v14;
|
||||
}
|
||||
|
@ -95,52 +95,3 @@ export async function which(
|
||||
}
|
||||
return programPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call which(programName) and spawn() with the given arguments.
|
||||
*
|
||||
* If returnExitCodeOrSignal is true, the returned promise will resolve to
|
||||
* an array [code, signal] with the child process exit code number or exit
|
||||
* signal string respectively (as provided by the spawn close event).
|
||||
*
|
||||
* If returnExitCodeOrSignal is false, the returned promise will reject with
|
||||
* a custom error if the child process returns a non-zero exit code or a
|
||||
* non-empty signal string (as reported by the spawn close event).
|
||||
*
|
||||
* In either case and if spawn itself emits an error event or fails synchronously,
|
||||
* the returned promise will reject with a custom error that includes the error
|
||||
* message of spawn's error.
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args: string[],
|
||||
options: import('child_process').SpawnOptions = { stdio: 'inherit' },
|
||||
returnExitCodeOrSignal = false,
|
||||
): Promise<[number | undefined, string | undefined]> {
|
||||
const { spawn } = await import('child_process');
|
||||
const program = await which(programName);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] [${program}, ${args.join(', ')}]`);
|
||||
}
|
||||
let error: Error | undefined;
|
||||
let exitCode: number | undefined;
|
||||
let exitSignal: string | undefined;
|
||||
try {
|
||||
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
|
||||
spawn(program, args, options)
|
||||
.on('error', reject)
|
||||
.on('close', (code, signal) => resolve([code, signal]));
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
if (error || (!returnExitCodeOrSignal && (exitCode || exitSignal))) {
|
||||
const msg = [
|
||||
`${programName} failed with exit code=${exitCode} signal=${exitSignal}:`,
|
||||
`[${program}, ${args.join(', ')}]`,
|
||||
...(error ? [`${error}`] : []),
|
||||
];
|
||||
throw new Error(msg.join('\n'));
|
||||
}
|
||||
return [exitCode, exitSignal];
|
||||
}
|
||||
|
6681
npm-shrinkwrap.json
generated
6681
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
129
package.json
129
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "12.55.0",
|
||||
"version": "13.4.1",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -51,8 +51,9 @@
|
||||
"build": "npm run build:src && npm run catch-uncommitted",
|
||||
"build:t": "npm run lint && npm run build:fast && npm run build:test",
|
||||
"build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion",
|
||||
"build:fast": "gulp pages && tsc && npx oclif-dev manifest",
|
||||
"build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit",
|
||||
"build:pages": "mkdirp ./build/auth/pages/&& inline-source --compress ./lib/auth/pages/error.ejs ./build/auth/pages/error.ejs && inline-source --compress ./lib/auth/pages/success.ejs ./build/auth/pages/success.ejs",
|
||||
"build:fast": "npm run build:pages && tsc && npx oclif manifest",
|
||||
"build:test": "tsc -P ./tsconfig.dev.json --noEmit",
|
||||
"build:doc": "ts-node --transpile-only automation/capitanodoc/index.ts > docs/balena-cli.md",
|
||||
"build:completion": "node completion/generate-completion.js",
|
||||
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
|
||||
@ -70,10 +71,9 @@
|
||||
"test:only": "npm run build:fast && cross-env BALENA_CLI_TEST_TYPE=source mocha \"tests/**/${npm_config_test}.spec.ts\"",
|
||||
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
|
||||
"ci": "npm run test && npm run catch-uncommitted",
|
||||
"watch": "gulp watch",
|
||||
"lint": "npm run lint-tsconfig && npm run lint-other",
|
||||
"lint-tsconfig": "balena-lint -e ts -e js -t tsconfig.dev.json --fix automation/ lib/ tests/ typings/",
|
||||
"lint-other": "balena-lint -e ts -e js --fix bin/balena bin/balena-dev completion/ gulpfile.js .mocharc.js .mocharc-standalone.js",
|
||||
"lint-other": "balena-lint -e ts -e js --fix bin/balena bin/balena-dev completion/ .mocharc.js .mocharc-standalone.js",
|
||||
"update": "ts-node --transpile-only ./automation/update-module.ts",
|
||||
"prepare": "echo {} > bin/.fast-boot.json",
|
||||
"prepublishOnly": "npm run build"
|
||||
@ -90,7 +90,7 @@
|
||||
"author": "Balena Inc. (https://balena.io/)",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10.20.0 <13.0.0",
|
||||
"node": ">=12.8.0 <13.0.0",
|
||||
"npm": "<7.0.0"
|
||||
},
|
||||
"husky": {
|
||||
@ -116,53 +116,53 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@balena/lint": "^6.2.0",
|
||||
"@oclif/config": "^1.17.0",
|
||||
"@oclif/dev-cli": "^1.26.0",
|
||||
"@oclif/parser": "^3.8.5",
|
||||
"@oclif/config": "^1.18.2",
|
||||
"@oclif/parser": "^3.8.6",
|
||||
"@octokit/plugin-throttling": "^3.5.1",
|
||||
"@octokit/rest": "^18.6.7",
|
||||
"@types/archiver": "^5.1.1",
|
||||
"@types/bluebird": "^3.5.36",
|
||||
"@types/body-parser": "^1.19.1",
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/cli-truncate": "^2.0.0",
|
||||
"@types/common-tags": "^1.8.1",
|
||||
"@types/dockerode": "^3.2.4",
|
||||
"@types/ejs": "^3.0.7",
|
||||
"@types/dockerode": "^3.3.8",
|
||||
"@types/ejs": "^3.1.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fs-extra": "^9.0.12",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/global-agent": "^2.1.1",
|
||||
"@types/global-tunnel-ng": "^2.1.1",
|
||||
"@types/http-proxy": "^1.17.7",
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"@types/intercept-stdout": "^0.1.0",
|
||||
"@types/is-root": "^2.1.2",
|
||||
"@types/js-yaml": "^4.0.2",
|
||||
"@types/jsonwebtoken": "^8.5.4",
|
||||
"@types/klaw": "^3.0.2",
|
||||
"@types/lodash": "^4.14.171",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.6",
|
||||
"@types/klaw": "^3.0.3",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mixpanel": "^2.14.3",
|
||||
"@types/mocha": "^8.2.3",
|
||||
"@types/mock-require": "^2.0.0",
|
||||
"@types/mock-require": "^2.0.1",
|
||||
"@types/moment-duration-format": "^2.2.3",
|
||||
"@types/ndjson": "^2.0.1",
|
||||
"@types/net-keepalive": "^0.4.1",
|
||||
"@types/nock": "^11.1.0",
|
||||
"@types/node": "^10.17.60",
|
||||
"@types/node-cleanup": "^2.1.1",
|
||||
"@types/parse-link-header": "^1.0.0",
|
||||
"@types/node": "^12.20.42",
|
||||
"@types/node-cleanup": "^2.1.2",
|
||||
"@types/parse-link-header": "^1.0.1",
|
||||
"@types/prettyjson": "^0.0.30",
|
||||
"@types/progress-stream": "^2.0.2",
|
||||
"@types/request": "^2.48.6",
|
||||
"@types/request": "^2.48.7",
|
||||
"@types/rewire": "^2.5.28",
|
||||
"@types/rimraf": "^3.0.1",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"@types/sinon": "^10.0.2",
|
||||
"@types/sinon": "^10.0.6",
|
||||
"@types/split": "^1.0.0",
|
||||
"@types/stream-to-promise": "^2.2.1",
|
||||
"@types/tar-stream": "^2.2.1",
|
||||
"@types/tar-stream": "^2.2.2",
|
||||
"@types/through2": "^2.0.36",
|
||||
"@types/tmp": "^0.2.1",
|
||||
"@types/tmp": "^0.2.3",
|
||||
"@types/which": "^2.0.1",
|
||||
"archiver": "^5.3.0",
|
||||
"catch-uncommitted": "^2.0.0",
|
||||
@ -173,115 +173,114 @@
|
||||
"diff": "^5.0.0",
|
||||
"electron-notarize": "^1.0.0",
|
||||
"ent": "^2.2.0",
|
||||
"filehound": "^1.17.4",
|
||||
"filehound": "^1.17.5",
|
||||
"fs-extra": "^9.1.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-inline-source": "^4.0.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"husky": "^4.3.8",
|
||||
"inline-source-cli": "^2.0.0",
|
||||
"intercept-stdout": "^0.1.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"mocha": "^8.4.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"nock": "^13.1.1",
|
||||
"nock": "^13.2.1",
|
||||
"parse-link-header": "^1.0.1",
|
||||
"pkg": "^4.4.9",
|
||||
"pkg": "^5.5.1",
|
||||
"publish-release": "^1.6.1",
|
||||
"rewire": "^5.0.0",
|
||||
"simple-git": "^2.40.0",
|
||||
"simple-git": "^2.48.0",
|
||||
"sinon": "^11.1.2",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^4.3.5"
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"@balena/es-version": "^1.0.0",
|
||||
"@oclif/command": "^1.8.0",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@oclif/command": "^1.8.16",
|
||||
"@resin.io/valid-email": "^0.1.0",
|
||||
"@sentry/node": "^6.13.2",
|
||||
"@sentry/node": "^6.16.1",
|
||||
"@types/fast-levenshtein": "0.0.1",
|
||||
"@types/update-notifier": "^4.1.1",
|
||||
"@zeit/dockerignore": "0.0.3",
|
||||
"JSONStream": "^1.0.3",
|
||||
"balena-config-json": "^4.2.0",
|
||||
"balena-device-init": "^6.0.0",
|
||||
"balena-errors": "^4.7.1",
|
||||
"balena-image-fs": "^7.0.6",
|
||||
"balena-image-manager": "^7.1.0",
|
||||
"balena-preload": "^11.0.0",
|
||||
"balena-image-manager": "^7.1.1",
|
||||
"balena-preload": "^12.0.0",
|
||||
"balena-release": "^3.2.0",
|
||||
"balena-sdk": "^15.51.1",
|
||||
"balena-sdk": "^16.9.0",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^4.0.7",
|
||||
"balena-settings-storage": "^7.0.0",
|
||||
"balena-sync": "^11.0.2",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"body-parser": "^1.19.1",
|
||||
"chalk": "^3.0.0",
|
||||
"chokidar": "^3.4.3",
|
||||
"chokidar": "^3.5.2",
|
||||
"cli-truncate": "^2.1.0",
|
||||
"color-hash": "^1.0.3",
|
||||
"cli-ux": "^5.5.1",
|
||||
"color-hash": "^1.1.1",
|
||||
"columnify": "^1.5.2",
|
||||
"common-tags": "^1.7.2",
|
||||
"denymount": "^2.3.0",
|
||||
"docker-modem": "^3.0.0",
|
||||
"docker-progress": "^5.0.0",
|
||||
"docker-modem": "3.0.0",
|
||||
"docker-progress": "^5.0.1",
|
||||
"docker-qemu-transpose": "^1.1.1",
|
||||
"dockerode": "^3.3.0",
|
||||
"ejs": "^3.1.3",
|
||||
"dockerode": "^3.3.1",
|
||||
"ejs": "^3.1.6",
|
||||
"etcher-sdk": "^6.2.1",
|
||||
"event-stream": "3.3.4",
|
||||
"express": "^4.13.3",
|
||||
"express": "^4.17.2",
|
||||
"fast-boot2": "^1.1.0",
|
||||
"fast-levenshtein": "^3.0.0",
|
||||
"filenamify": "^4.3.0",
|
||||
"get-stdin": "^8.0.0",
|
||||
"glob": "^7.1.7",
|
||||
"global-agent": "^2.1.12",
|
||||
"glob": "^7.2.0",
|
||||
"global-agent": "^2.2.0",
|
||||
"global-tunnel-ng": "^2.1.1",
|
||||
"got": "^11.8.2",
|
||||
"got": "^11.8.3",
|
||||
"humanize": "0.0.9",
|
||||
"ignore": "^5.1.8",
|
||||
"inquirer": "^7.3.3",
|
||||
"is-elevated": "^3.0.0",
|
||||
"is-root": "^2.1.0",
|
||||
"js-yaml": "^4.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"klaw": "^3.0.0",
|
||||
"livepush": "^3.5.0",
|
||||
"livepush": "^3.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"minimatch": "^3.0.4",
|
||||
"moment": "^2.27.0",
|
||||
"moment": "^2.29.1",
|
||||
"moment-duration-format": "^2.3.2",
|
||||
"ndjson": "^2.0.0",
|
||||
"net-keepalive": "^3.0.0",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"node-unzip-2": "^0.2.8",
|
||||
"oclif": "^1.18.1",
|
||||
"oclif": "^1.18.4",
|
||||
"open": "^7.1.0",
|
||||
"patch-package": "^6.4.7",
|
||||
"prettyjson": "^1.1.3",
|
||||
"prettyjson": "^1.2.5",
|
||||
"progress-stream": "^2.0.0",
|
||||
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
|
||||
"request": "^2.88.2",
|
||||
"resin-cli-form": "^2.0.2",
|
||||
"resin-cli-visuals": "^1.8.0",
|
||||
"resin-compose-parse": "^2.1.3",
|
||||
"resin-doodles": "^0.1.1",
|
||||
"resin-doodles": "^0.2.0",
|
||||
"resin-multibuild": "^4.12.2",
|
||||
"resin-stream-logger": "^0.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.2",
|
||||
"semver": "^7.3.5",
|
||||
"shell-escape": "^0.2.0",
|
||||
"split": "^1.0.1",
|
||||
"stream-to-promise": "^2.2.0",
|
||||
"string-width": "^4.2.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi-stream": "^1.0.0",
|
||||
"tar-stream": "^2.1.3",
|
||||
"tar-utils": "^2.1.1",
|
||||
"through2": "^2.0.3",
|
||||
"tmp": "^0.2.1",
|
||||
"typed-error": "^3.2.1",
|
||||
"update-notifier": "^4.1.0",
|
||||
"update-notifier": "^5.1.0",
|
||||
"which": "^2.0.2",
|
||||
"window-size": "^1.1.0"
|
||||
},
|
||||
@ -289,6 +288,6 @@
|
||||
"windosu": "^0.3.0"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2021-11-29T10:48:16.348Z"
|
||||
"publishedAt": "2022-04-11T16:10:47.566Z"
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
diff --git a/node_modules/@oclif/plugin-help/lib/command.js b/node_modules/@oclif/plugin-help/lib/command.js
|
||||
index 1caa65b..e205faf 100644
|
||||
index b3b9010..788e5c6 100644
|
||||
--- a/node_modules/@oclif/plugin-help/lib/command.js
|
||||
+++ b/node_modules/@oclif/plugin-help/lib/command.js
|
||||
@@ -87,7 +87,7 @@ class CommandHelp {
|
||||
if (args.filter(a => a.description).length === 0)
|
||||
@@ -88,7 +88,7 @@ class CommandHelp {
|
||||
return;
|
||||
const body = list_1.renderList(args.map(a => {
|
||||
var _a;
|
||||
- const name = a.name.toUpperCase();
|
||||
+ const name = a.required ? `<${a.name}>` : `[${a.name}]`;
|
||||
let description = a.description || '';
|
||||
if (a.default)
|
||||
description = `[default: ${a.default}] ${description}`;
|
||||
@@ -130,9 +130,7 @@ class CommandHelp {
|
||||
// `a.default` is actually not always a string (typing bug), hence `toString()`
|
||||
if (a.default || ((_a = a.default) === null || _a === void 0 ? void 0 : _a.toString()) === '0')
|
||||
@@ -133,9 +133,7 @@ class CommandHelp {
|
||||
if (!flag.helpValue && flag.options) {
|
||||
value = flag.options.join('|');
|
||||
}
|
||||
@ -21,12 +21,12 @@ index 1caa65b..e205faf 100644
|
||||
+ left += ` <${value}>`;
|
||||
}
|
||||
let right = flag.description || '';
|
||||
if (flag.type === 'option' && flag.default) {
|
||||
// `flag.default` is not always a string (typing bug), hence `toString()`
|
||||
diff --git a/node_modules/@oclif/plugin-help/lib/index.js b/node_modules/@oclif/plugin-help/lib/index.js
|
||||
index 02646b6..525e19f 100644
|
||||
index 04d7861..c2fb591 100644
|
||||
--- a/node_modules/@oclif/plugin-help/lib/index.js
|
||||
+++ b/node_modules/@oclif/plugin-help/lib/index.js
|
||||
@@ -95,11 +95,12 @@ class Help extends HelpBase {
|
||||
@@ -98,11 +98,12 @@ class Help extends HelpBase {
|
||||
console.log(title + '\n');
|
||||
console.log(this.formatCommand(command));
|
||||
console.log('');
|
@ -1,20 +1,20 @@
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js b/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
|
||||
index e0abbbe..debf799 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
|
||||
@@ -128,6 +128,7 @@ class PackMacos extends command_1.Command {
|
||||
diff --git a/node_modules/oclif/lib/commands/pack/macos.js b/node_modules/oclif/lib/commands/pack/macos.js
|
||||
index 924f092..a69e60b 100644
|
||||
--- a/node_modules/oclif/lib/commands/pack/macos.js
|
||||
+++ b/node_modules/oclif/lib/commands/pack/macos.js
|
||||
@@ -133,6 +133,7 @@ class PackMacos extends command_1.Command {
|
||||
if (process.env.OSX_KEYCHAIN)
|
||||
args.push('--keychain', process.env.OSX_KEYCHAIN);
|
||||
args.push(dist);
|
||||
+ console.error(`[debug] @oclif/dev-cli pkgbuild "${args.join('" "')}"`);
|
||||
+ console.error(`[debug] oclif pkgbuild "${args.join('" "')}"`);
|
||||
await qq.x('pkgbuild', args);
|
||||
}
|
||||
}
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
|
||||
index a313991..6681892 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
|
||||
@@ -51,6 +51,13 @@ VIAddVersionKey /LANG=\${LANG_ENGLISH} "ProductVersion" "\${VERSION}.0"
|
||||
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
|
||||
index bf4657e..fd58c7d 100644
|
||||
--- a/node_modules/oclif/lib/commands/pack/win.js
|
||||
+++ b/node_modules/oclif/lib/commands/pack/win.js
|
||||
@@ -52,6 +52,13 @@ VIAddVersionKey /LANG=\${LANG_ENGLISH} "ProductVersion" "\${VERSION}.0"
|
||||
InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
|
||||
|
||||
Section "${config.name} CLI \${VERSION}"
|
||||
@ -28,44 +28,53 @@ index a313991..6681892 100644
|
||||
SetOutPath $INSTDIR
|
||||
File /r bin
|
||||
File /r client
|
||||
@@ -192,7 +199,8 @@ class PackWin extends command_1.Command {
|
||||
@@ -61,6 +68,8 @@ Section "${config.name} CLI \${VERSION}"
|
||||
WriteUninstaller "$INSTDIR\\Uninstall.exe"
|
||||
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
|
||||
"DisplayName" "${config.name}"
|
||||
+ WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
|
||||
+ "DisplayVersion" "\${VERSION}"
|
||||
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
|
||||
"UninstallString" "$\\"$INSTDIR\\uninstall.exe$\\""
|
||||
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
|
||||
@@ -193,7 +202,8 @@ class PackWin extends command_1.Command {
|
||||
async run() {
|
||||
await this.checkForNSIS();
|
||||
const { flags } = this.parse(PackWin);
|
||||
- const buildConfig = await Tarballs.buildConfig(flags.root);
|
||||
+ const targets = flags.targets ? flags.targets.split(',') : undefined;
|
||||
+ const buildConfig = await Tarballs.buildConfig(flags.root, { targets });
|
||||
const { config } = buildConfig;
|
||||
+ const $targets = flags.targets ? flags.targets.split(',') : undefined;
|
||||
+ const buildConfig = await Tarballs.buildConfig(flags.root, { targets: $targets });
|
||||
const { config, version, gitSha, targets, tmp } = buildConfig;
|
||||
await Tarballs.build(buildConfig, { platform: 'win32', pack: false });
|
||||
const arches = buildConfig.targets.filter(t => t.platform === 'win32').map(t => t.arch);
|
||||
@@ -207,7 +215,8 @@ class PackWin extends command_1.Command {
|
||||
const arches = targets.filter(t => t.platform === 'win32').map(t => t.arch);
|
||||
@@ -208,7 +218,8 @@ class PackWin extends command_1.Command {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await qq.mv(buildConfig.workspace({ platform: 'win32', arch }), [installerBase, 'client']);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
- await qq.x(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
|
||||
+ const { msysExec, toMsysPath } = require("../../util");
|
||||
+ await msysExec(`makensis ${toMsysPath(installerBase)}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
|
||||
const o = buildConfig.dist(`win/${config.bin}-v${buildConfig.version}-${arch}.exe`);
|
||||
const templateKey = upload_util_1.templateShortKey('win32', { bin: config.bin, version: version, sha: gitSha, arch });
|
||||
const o = buildConfig.dist(`win32/${templateKey}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await qq.mv([installerBase, 'installer.exe'], o);
|
||||
@@ -232,4 +241,5 @@ exports.default = PackWin;
|
||||
@@ -255,4 +266,5 @@ PackWin.hidden = true;
|
||||
PackWin.description = 'create windows installer from oclif CLI';
|
||||
PackWin.flags = {
|
||||
root: command_1.flags.string({ char: 'r', description: 'path to oclif CLI root', default: '.', required: true }),
|
||||
+ targets: command_1.flags.string({char: 't', description: 'comma-separated targets to pack (e.g.: win32-x86,win32-x64)'}),
|
||||
};
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/build.js b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
|
||||
index c6bd245..baa7f6f 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/tarballs/build.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
|
||||
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
|
||||
index d3e8e89..a5d29e2 100644
|
||||
--- a/node_modules/oclif/lib/tarballs/build.js
|
||||
+++ b/node_modules/oclif/lib/tarballs/build.js
|
||||
@@ -18,8 +18,9 @@ const pack = async (from, to) => {
|
||||
qq.cd(prevCwd);
|
||||
};
|
||||
async function build(c, options = {}) {
|
||||
- const { xz, config } = c;
|
||||
+ const { xz, config, tmp } = c;
|
||||
- const { xz, config, version, s3Config, gitSha, nodeVersion, targets, updateConfig } = c;
|
||||
+ const { xz, config, version, s3Config, gitSha, nodeVersion, targets, updateConfig, tmp } = c;
|
||||
const prevCwd = qq.cwd();
|
||||
+ console.error(`[debug] @oclif/dev-cli cwd="${prevCwd}"\n c.root="${c.root}" c.workspace()="${c.workspace()}"`);
|
||||
+ console.error(`[debug] oclif cwd="${prevCwd}"\n c.root="${c.root}" c.workspace()="${c.workspace()}"`);
|
||||
const packCLI = async () => {
|
||||
const stdout = await qq.x.stdout('npm', ['pack', '--unsafe-perm'], { cwd: c.root });
|
||||
return path.join(c.root, stdout.split('\n').pop());
|
||||
@ -110,7 +119,7 @@ index c6bd245..baa7f6f 100644
|
||||
- await qq.x('npm install --production');
|
||||
+ const ws = c.workspace();
|
||||
+ qq.cd(ws);
|
||||
+ console.error(`[debug] @oclif/dev-cli copying node_modules to "${ws}"`)
|
||||
+ console.error(`[debug] oclif copying node_modules to "${ws}"`)
|
||||
+ const source = path.join(c.root, 'node_modules');
|
||||
+ if (process.platform === 'win32') {
|
||||
+ // xcopy is much faster than `qq.cp(source, ws)`
|
||||
@ -120,13 +129,13 @@ index c6bd245..baa7f6f 100644
|
||||
+ // file attributes containing `codesign` digital signatures
|
||||
+ await qq.x(`cp -pR "${source}" "${ws}"`);
|
||||
}
|
||||
+ console.error(`[debug] @oclif/dev-cli running "npm prune --production" in "${ws}"`);
|
||||
+ console.error(`[debug] oclif running "npm prune --production" in "${ws}"`);
|
||||
+ await qq.x('npm prune --production');
|
||||
+ console.error(`[debug] @oclif/dev-cli done`);
|
||||
+ console.error(`[debug] oclif done`);
|
||||
};
|
||||
const buildTarget = async (target) => {
|
||||
const workspace = c.workspace(target);
|
||||
@@ -74,7 +83,8 @@ async function build(c, options = {}) {
|
||||
const pretarball = async () => {
|
||||
qq.cd(c.workspace());
|
||||
@@ -99,7 +108,8 @@ async function build(c, options = {}) {
|
||||
output: path.join(workspace, 'bin', 'node'),
|
||||
platform: target.platform,
|
||||
arch: target.arch,
|
||||
@ -136,11 +145,11 @@ index c6bd245..baa7f6f 100644
|
||||
});
|
||||
if (options.pack === false)
|
||||
return;
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/config.js b/node_modules/@oclif/dev-cli/lib/tarballs/config.js
|
||||
index 9754a6b..68ef6b7 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/tarballs/config.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/config.js
|
||||
@@ -17,7 +17,10 @@ function gitSha(cwd, options = {}) {
|
||||
diff --git a/node_modules/oclif/lib/tarballs/config.js b/node_modules/oclif/lib/tarballs/config.js
|
||||
index 0dc3cd7..1336219 100644
|
||||
--- a/node_modules/oclif/lib/tarballs/config.js
|
||||
+++ b/node_modules/oclif/lib/tarballs/config.js
|
||||
@@ -18,7 +18,10 @@ function gitSha(cwd, options = {}) {
|
||||
}
|
||||
exports.gitSha = gitSha;
|
||||
async function Tmp(config) {
|
||||
@ -148,23 +157,23 @@ index 9754a6b..68ef6b7 100644
|
||||
+ const tmp = process.env.BUILD_TMP
|
||||
+ ? path.join(process.env.BUILD_TMP, 'oclif')
|
||||
+ : path.join(config.root, 'tmp');
|
||||
+ console.error(`[debug] @oclif/dev-cli tmp="${tmp}"`);
|
||||
+ console.error(`[debug] oclif tmp="${tmp}"`);
|
||||
await qq.mkdirp(tmp);
|
||||
return tmp;
|
||||
}
|
||||
@@ -44,7 +47,7 @@ async function buildConfig(root, options = {}) {
|
||||
@@ -43,7 +46,7 @@ async function buildConfig(root, options = {}) {
|
||||
s3Config: updateConfig.s3,
|
||||
nodeVersion: updateConfig.node.version || process.versions.node,
|
||||
workspace(target) {
|
||||
- const base = qq.join(config.root, 'tmp');
|
||||
+ const base = tmp;
|
||||
if (target && target.platform)
|
||||
return qq.join(base, [target.platform, target.arch].join('-'), config.s3Key('baseDir', target));
|
||||
return qq.join(base, config.s3Key('baseDir', target));
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/node.js b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
|
||||
return qq.join(base, [target.platform, target.arch].join('-'), upload_util_1.templateShortKey('baseDir', { bin: config.bin }));
|
||||
return qq.join(base, upload_util_1.templateShortKey('baseDir', { bin: config.bin }));
|
||||
diff --git a/node_modules/oclif/lib/tarballs/node.js b/node_modules/oclif/lib/tarballs/node.js
|
||||
index fabe5c4..e32dd76 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/tarballs/node.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
|
||||
--- a/node_modules/oclif/lib/tarballs/node.js
|
||||
+++ b/node_modules/oclif/lib/tarballs/node.js
|
||||
@@ -4,9 +4,10 @@ const errors_1 = require("@oclif/errors");
|
||||
const path = require("path");
|
||||
const qq = require("qqjs");
|
||||
@ -196,14 +205,33 @@ index fabe5c4..e32dd76 100644
|
||||
await qq.mv([nodeBase, 'node.exe'], cache);
|
||||
qq.popd();
|
||||
}
|
||||
diff --git a/node_modules/@oclif/dev-cli/lib/util.js b/node_modules/@oclif/dev-cli/lib/util.js
|
||||
index b3d48b7..540bbe6 100644
|
||||
--- a/node_modules/@oclif/dev-cli/lib/util.js
|
||||
+++ b/node_modules/@oclif/dev-cli/lib/util.js
|
||||
@@ -40,3 +40,47 @@ function sortBy(arr, fn) {
|
||||
}
|
||||
exports.sortBy = sortBy;
|
||||
exports.template = (context) => (t) => _.template(t || '')(context);
|
||||
diff --git a/node_modules/oclif/lib/upload-util.js b/node_modules/oclif/lib/upload-util.js
|
||||
index 45392cb..3c806c7 100644
|
||||
--- a/node_modules/oclif/lib/upload-util.js
|
||||
+++ b/node_modules/oclif/lib/upload-util.js
|
||||
@@ -28,10 +28,10 @@ function templateShortKey(type, ext, options = { root: '.' }) {
|
||||
const templates = {
|
||||
baseDir: '<%- bin %>',
|
||||
unversioned: '<%- bin %>-<%- platform %>-<%- arch %><%- ext %>',
|
||||
- versioned: '<%- bin %>-v<%- version %>-<%- sha %>-<%- platform %>-<%- arch %><%- ext %>',
|
||||
- manifest: '<%- bin %>-v<%- version %>-<%- sha %>-<%- platform %>-<%- arch %>-buildmanifest',
|
||||
- macos: '<%- bin %>-v<%- version %>-<%- sha %>.pkg',
|
||||
- win32: '<%- bin %>-v<%- version %>-<%- sha %>-<%- arch %>.exe',
|
||||
+ versioned: '<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>',
|
||||
+ manifest: '<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %>-buildmanifest',
|
||||
+ macos: '<%- bin %>-v<%- version %>.pkg',
|
||||
+ win32: '<%- bin %>-v<%- version %>-<%- arch %>.exe',
|
||||
deb: '<%- bin %>_<%- versionShaRevision %>_<%- arch %>.deb',
|
||||
};
|
||||
return _.template(templates[type])(Object.assign({}, options));
|
||||
diff --git a/node_modules/oclif/lib/util.js b/node_modules/oclif/lib/util.js
|
||||
index 17748ad..4928fc9 100644
|
||||
--- a/node_modules/oclif/lib/util.js
|
||||
+++ b/node_modules/oclif/lib/util.js
|
||||
@@ -67,3 +67,47 @@ exports.sortVersionsObjectByKeysDesc = (input) => {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
+
|
||||
+// OSTYPE is 'msys' for MSYS 1.0 and for MSYS2, or 'cygwin' for Cygwin
|
||||
+// but note that OSTYPE is not "exported" by default, so run: export OSTYPE=$OSTYPE
|
||||
@ -219,7 +247,7 @@ index b3d48b7..540bbe6 100644
|
||||
+exports.isCygwin = isCygwin;
|
||||
+exports.isMinGW = isMinGW;
|
||||
+exports.isMSYS2 = isMSYS2;
|
||||
+console.error(`[debug] @oclif/dev-cli MSYSSHELLPATH=${MSYSSHELLPATH} MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
|
||||
+console.error(`[debug] oclif MSYSSHELLPATH=${MSYSSHELLPATH} MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
|
||||
+
|
||||
+const qq = require("qqjs");
|
||||
+
|
||||
@ -232,7 +260,7 @@ index b3d48b7..540bbe6 100644
|
||||
+ } else if (isCygwin) {
|
||||
+ msysPath = msysPath.replace(/^([a-zA-Z]):/, '/cygdrive/$1');
|
||||
+ }
|
||||
+ console.error(`[debug] @oclif/dev-cli toMsysPath before="${windowsPath}" after="${msysPath}"`);
|
||||
+ console.error(`[debug] oclif toMsysPath before="${windowsPath}" after="${msysPath}"`);
|
||||
+ return msysPath;
|
||||
+}
|
||||
+exports.toMsysPath = toMsysPath;
|
||||
@ -244,7 +272,7 @@ index b3d48b7..540bbe6 100644
|
||||
+ }
|
||||
+ const sh = MSYSSHELLPATH;
|
||||
+ const args = ['-c', cmd];
|
||||
+ console.error(`[debug] @oclif/dev-cli msysExec sh="${sh}" args=${JSON.stringify(args)} options=${JSON.stringify(options)}`);
|
||||
+ console.error(`[debug] oclif msysExec sh="${sh}" args=${JSON.stringify(args)} options=${JSON.stringify(options)}`);
|
||||
+ return qq.x(sh, args, options);
|
||||
+}
|
||||
+exports.msysExec = msysExec;
|
@ -1,37 +0,0 @@
|
||||
diff --git a/node_modules/pkg/lib-es5/packer.js b/node_modules/pkg/lib-es5/packer.js
|
||||
index 7295bb6..76805a3 100644
|
||||
--- a/node_modules/pkg/lib-es5/packer.js
|
||||
+++ b/node_modules/pkg/lib-es5/packer.js
|
||||
@@ -128,6 +128,7 @@ function _default({
|
||||
const newStat = Object.assign({}, value);
|
||||
newStat.isFileValue = value.isFile();
|
||||
newStat.isDirectoryValue = value.isDirectory();
|
||||
+ newStat.isSocketValue = value.isSocket();
|
||||
const buffer = Buffer.from(JSON.stringify(newStat));
|
||||
stripes.push({
|
||||
snap,
|
||||
diff --git a/node_modules/pkg/prelude/bootstrap.js b/node_modules/pkg/prelude/bootstrap.js
|
||||
index 0d19f1d..db69015 100644
|
||||
--- a/node_modules/pkg/prelude/bootstrap.js
|
||||
+++ b/node_modules/pkg/prelude/bootstrap.js
|
||||
@@ -925,8 +925,10 @@ function payloadFileSync (pointer) {
|
||||
|
||||
var isFileValue = s.isFileValue;
|
||||
var isDirectoryValue = s.isDirectoryValue;
|
||||
+ var isSocketValue = s.isSocketValue;
|
||||
delete s.isFileValue;
|
||||
delete s.isDirectoryValue;
|
||||
+ delete s.isSocketValue;
|
||||
|
||||
s.isFile = function () {
|
||||
return isFileValue;
|
||||
@@ -934,6 +936,9 @@ function payloadFileSync (pointer) {
|
||||
s.isDirectory = function () {
|
||||
return isDirectoryValue;
|
||||
};
|
||||
+ s.isSocket = function () {
|
||||
+ return isSocketValue;
|
||||
+ };
|
||||
s.isSymbolicLink = function () {
|
||||
return false;
|
||||
};
|
@ -24,7 +24,10 @@ const { promisify } = require('util');
|
||||
const execFileAsync = promisify(execFile);
|
||||
const patchesDir = 'patches';
|
||||
|
||||
/** Run the patch-package tool in a child process and wait for it to finish */
|
||||
/**
|
||||
* Run the patch-package tool in a child process and wait for it to finish
|
||||
* @param {string} patchDir
|
||||
*/
|
||||
async function patchPackage(patchDir) {
|
||||
// Equivalent to: `npx patch-package --patch-dir $patchDir`
|
||||
const result = await execFileAsync('node', [
|
||||
|
2
repo.yml
2
repo.yml
@ -16,3 +16,5 @@ upstream:
|
||||
url: 'https://github.com/balena-io-modules/etcher-sdk/'
|
||||
- repo: 'resin-compose-parse'
|
||||
url: 'https://github.com/balena-io-modules/resin-compose-parse'
|
||||
- repo: 'docker-progress'
|
||||
url: 'https://github.com/balena-io-modules/docker-progress'
|
||||
|
@ -22,7 +22,6 @@ import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { stripIndent } from '../../build/utils/lazy';
|
||||
import { isV13 } from '../../build/utils/version';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
||||
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
|
||||
@ -127,23 +126,14 @@ describe('balena build', function () {
|
||||
];
|
||||
if (isWindows) {
|
||||
const fname = path.join(projectPath, 'src', 'windows-crlf.sh');
|
||||
if (isWindows) {
|
||||
expectedResponseLines.push(
|
||||
`[Info] Converting line endings CRLF -> LF for file: ${fname}`,
|
||||
);
|
||||
} else {
|
||||
expectedResponseLines.push(
|
||||
`[Warn] CRLF (Windows) line endings detected in file: ${fname}`,
|
||||
'[Warn] Windows-format line endings were detected in some files. Consider using the `--convert-eol` option.',
|
||||
);
|
||||
}
|
||||
expectedResponseLines.push(
|
||||
`[Info] Converting line endings CRLF -> LF for file: ${fname}`,
|
||||
);
|
||||
}
|
||||
docker.expectGetInfo({});
|
||||
docker.expectGetManifestBusybox();
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 ${
|
||||
isV13() ? '' : '-g'
|
||||
}`,
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: {
|
||||
@ -192,16 +182,9 @@ describe('balena build', function () {
|
||||
];
|
||||
if (isWindows) {
|
||||
const fname = path.join(projectPath, 'src', 'windows-crlf.sh');
|
||||
if (isWindows) {
|
||||
expectedResponseLines.push(
|
||||
`[Info] Converting line endings CRLF -> LF for file: ${fname}`,
|
||||
);
|
||||
} else {
|
||||
expectedResponseLines.push(
|
||||
`[Warn] CRLF (Windows) line endings detected in file: ${fname}`,
|
||||
'[Warn] Windows-format line endings were detected in some files. Consider using the `--convert-eol` option.',
|
||||
);
|
||||
}
|
||||
expectedResponseLines.push(
|
||||
`[Info] Converting line endings CRLF -> LF for file: ${fname}`,
|
||||
);
|
||||
}
|
||||
docker.expectGetInfo({});
|
||||
docker.expectGetManifestBusybox();
|
||||
@ -297,9 +280,7 @@ describe('balena build', function () {
|
||||
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });
|
||||
docker.expectGetManifestBusybox();
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch} ${
|
||||
isV13() ? '' : '--nogitignore'
|
||||
}`,
|
||||
commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch}`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: {
|
||||
@ -317,7 +298,7 @@ describe('balena build', function () {
|
||||
}
|
||||
});
|
||||
|
||||
it('should create the expected tar stream (single container, --[no]convert-eol, --multi-dockerignore)', async () => {
|
||||
it('should create the expected tar stream (single container, --noconvert-eol, --multi-dockerignore)', async () => {
|
||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||
const expectedFiles: ExpectedTarStreamFiles = {
|
||||
'src/.dockerignore': { fileSize: 16, type: 'file' },
|
||||
@ -448,9 +429,7 @@ describe('balena build', function () {
|
||||
docker.expectGetManifestNucAlpine();
|
||||
docker.expectGetManifestBusybox();
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol ${
|
||||
isV13() ? '' : '-G'
|
||||
} -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService,
|
||||
expectedQueryParamsByService,
|
||||
@ -533,7 +512,7 @@ describe('balena build', function () {
|
||||
docker.expectGetManifestNucAlpine();
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m`,
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -m`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService,
|
||||
expectedQueryParamsByService,
|
||||
@ -618,7 +597,7 @@ describe('balena build', function () {
|
||||
docker.expectGetManifestNucAlpine();
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m --tag ${tag} --projectName ${projectName}`,
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -m --tag ${tag} --projectName ${projectName}`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService,
|
||||
expectedQueryParamsByService,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user