mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
234 Commits
v12.48.0
...
output-fra
Author | SHA1 | Date | |
---|---|---|---|
ab1d8aa6ba | |||
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 | |||
e431a59af7 | |||
41a2dbe60c | |||
6ba67eefdb | |||
3b885ad906 | |||
5574dc0318 | |||
fcea91bfb6 | |||
7316c4e075 | |||
389b7a1463 | |||
09d004423c | |||
97978ff812 | |||
498e21f0ab | |||
257dd514ed | |||
85cbdd4947 | |||
73625611da | |||
d2a5a9ba86 | |||
1cd78215e0 | |||
6d744d0b07 | |||
9d312bcd12 | |||
e22aa847e3 | |||
0d1ca67d5b | |||
c4a5a25f03 | |||
b183d88400 | |||
2b6a2142eb | |||
58b29bf4bb | |||
fc0903a414 | |||
cea23f5d5e | |||
5a9b5e3b08 | |||
52138d41eb | |||
5acdc63068 | |||
b546e4dd97 | |||
e4870916e2 | |||
3ca93448cd | |||
f66395e2d5 | |||
952d782e90 | |||
d53c9b3c50 | |||
2f706c0200 | |||
d64b6deb81 | |||
55fc9b2ade | |||
6c29d0ae27 | |||
f46452f6de | |||
c166ec7597 | |||
7325c79888 | |||
2a29b386eb | |||
23b07f8a41 | |||
6d641b4841 | |||
7b498149b1 | |||
ae5ea0f4e8 | |||
f635f648da | |||
3d4e2cf823 | |||
ef3b630887 | |||
19040ccb6c | |||
8e712ac910 | |||
c401ed35ac | |||
94be97313b | |||
48053ecefc | |||
cc60e86507 | |||
bd774e8553 | |||
c493c33e38 | |||
9487b33144 | |||
befdae1b90 | |||
08dfc945f3 | |||
8791c2f4e1 | |||
be306e6a20 | |||
6cfff72c59 | |||
adae718c2e | |||
132e1a63b2 | |||
a18e182ae4 | |||
e098cdca17 | |||
b42af74983 | |||
8bb211e441 | |||
ffccbfba12 | |||
56c1af50c0 | |||
8b9e3ccdc8 | |||
de95262f93 | |||
ed49938504 | |||
52ad0f6a57 | |||
7f6738c73c | |||
88fc3f7714 | |||
1afb29b923 | |||
09a4e8db2d | |||
6c81440428 | |||
3eca65ce0d | |||
6319b9dc13 | |||
290acaecbb | |||
305c9045f0 | |||
b701151769 | |||
e03bbb7275 | |||
3fd66c39ae | |||
b30075a18b | |||
a4fc95e99b | |||
63d8e5e6a3 | |||
6244af3464 | |||
8773927b3f | |||
29a3fd40a2 | |||
d6faf060e6 | |||
352fd197b7 | |||
afb6f938b7 | |||
d3adbcdba9 | |||
33fce1f24f | |||
ab90a5f150 | |||
a8b2212fed | |||
6bb8df30dd | |||
0327ed766d | |||
1009958340 | |||
5ce17ea70f | |||
9c821511b1 | |||
d793335287 | |||
dc59b7e4b0 | |||
370b844538 | |||
a8c2724929 | |||
09dd2dd354 | |||
f3ab41841a | |||
3dee30a0fe | |||
d34073f695 | |||
24fe6666e4 | |||
3fd5981085 | |||
08ee8643cb | |||
8db36ccec9 | |||
deb3e4c4ac | |||
a8ff21af69 | |||
112a7b8194 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -6,7 +6,7 @@
|
||||
*.sh text eol=lf
|
||||
|
||||
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
|
||||
doc/cli.markdown text eol=lf
|
||||
docs/balena-cli.md text eol=lf
|
||||
# crlf for the eol conversion test files
|
||||
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
|
||||
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf
|
||||
|
@ -3,5 +3,8 @@ module.exports = {
|
||||
require: 'ts-node/register/transpile-only',
|
||||
file: './tests/config-tests',
|
||||
timeout: 12000,
|
||||
// To test only, say, 'push.spec.ts', do it as follows so that
|
||||
// requests are authenticated:
|
||||
// spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],
|
||||
spec: 'tests/**/*.spec.ts',
|
||||
};
|
||||
|
@ -5,13 +5,11 @@ npm:
|
||||
os: ubuntu
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
|
File diff suppressed because it is too large
Load Diff
795
CHANGELOG.md
795
CHANGELOG.md
@ -4,6 +4,801 @@ 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.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]
|
||||
|
||||
## 12.54.5 - 2021-11-27
|
||||
|
||||
* os configure, local configure: Reuse disk partition scanning logic [Paulo Castro]
|
||||
|
||||
## 12.54.4 - 2021-11-26
|
||||
|
||||
* Bump 'balena-lint' and fix 'no-floating-promises' warnings [Paulo Castro]
|
||||
|
||||
## 12.54.3 - 2021-11-26
|
||||
|
||||
* Improve directory scan speed prior to tarballing [Lucian Buzzo]
|
||||
|
||||
## 12.54.2 - 2021-11-26
|
||||
|
||||
* Set the correct target state when using Compose "image" field [Lucian Buzzo]
|
||||
|
||||
## 12.54.1 - 2021-11-25
|
||||
|
||||
* Fix mistake in `config generate` examples [Scott Lowe]
|
||||
|
||||
## 12.54.0 - 2021-11-25
|
||||
|
||||
* Improve UX for offline usage [Scott Lowe]
|
||||
|
||||
## 12.53.2 - 2021-11-25
|
||||
|
||||
* Avoid creating an extra provisioning API key in os configure & config generate [Thodoris Greasidis]
|
||||
|
||||
## 12.53.1 - 2021-11-25
|
||||
|
||||
* Transitional changes to doc files for landr implementation [Scott Lowe]
|
||||
|
||||
## 12.53.0 - 2021-11-25
|
||||
|
||||
* config read/write/inject: Avoid need for internet access [Paulo Castro]
|
||||
* config read: Add '--json' option for JSON output [Paulo Castro]
|
||||
|
||||
## 12.52.2 - 2021-11-24
|
||||
|
||||
* Delete 'doc/automated-init.md' and improve 'balena help device init' [Paulo Castro]
|
||||
|
||||
## 12.52.1 - 2021-11-22
|
||||
|
||||
* push/build: Add test cases for .dockerignore filtering corner cases [Paulo Castro]
|
||||
|
||||
## 12.52.0 - 2021-11-20
|
||||
|
||||
* os download: Display OS version actually downloaded (range or 'recommended') [Paulo Castro]
|
||||
* os versions, os download: Add support for balenaOS ESR versions [Paulo Castro]
|
||||
|
||||
## 12.51.3 - 2021-11-16
|
||||
|
||||
* deploy: Ensure the release fails if an image's digest (hash) is missing [Paulo Castro]
|
||||
|
||||
## 12.51.2 - 2021-11-16
|
||||
|
||||
* Update balena CI configuration (remove Node v10 from npm pipeline list) [Paulo Castro]
|
||||
|
||||
## 12.51.1 - 2021-10-25
|
||||
|
||||
* Fix forums support link in README.md [Scott Lowe]
|
||||
|
||||
## 12.51.0 - 2021-10-22
|
||||
|
||||
* Add support for YAML anchors and aliases in 'docker-compose.yml' [dfunckt]
|
||||
|
||||
## 12.50.3 - 2021-10-20
|
||||
|
||||
|
||||
<details>
|
||||
<summary> preload: Avoid possible ValueError when parsing storage driver [Kyle Harding] </summary>
|
||||
|
||||
> ### balena-preload-11.0.0 - 2021-10-13
|
||||
>
|
||||
> * Avoid creating multiple preload containers [Kyle Harding]
|
||||
> * major: Remove balena-preload script in favor of use with CLI [Lorenzo Alberto Maria Ambrosi]
|
||||
> * Fix missing 'await' for getEdisonPartitions() [Paulo Castro]
|
||||
> * Add extra type information (refactor bind mount array) [Paulo Castro]
|
||||
> * Run linter [Paulo Castro]
|
||||
> * major: Convert to typescript [Lorenzo Alberto Maria Ambrosi]
|
||||
> * patch: Fix incorrect python List index check [Lorenzo Alberto Maria Ambrosi]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.50.2 - 2021-10-05
|
||||
|
||||
* Error message when renaming a fleet now mentions the target name. [Carlo Miguel F. Cruz]
|
||||
|
||||
## 12.50.1 - 2021-09-30
|
||||
|
||||
* Update dependencies (@sentry/node error reporting) [Paulo Castro]
|
||||
* Replace mixpanel dependency with simple GET request [Paulo Castro]
|
||||
* Avoid NockMock warnings during standalone executable testing [Paulo Castro]
|
||||
* Fix help output for 'release finalize' command [Paulo Castro]
|
||||
|
||||
## 12.50.0 - 2021-09-28
|
||||
|
||||
* Add support for releases [Paul Jonathan Zoulin]
|
||||
|
||||
## 12.49.0 - 2021-09-23
|
||||
|
||||
* build, deploy: Improve logging of image build messages [Paulo Castro]
|
||||
* build, deploy: Add support for multiarch base images [toochevere]
|
||||
|
||||
## 12.48.15 - 2021-09-22
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to 15.51.1 [Nitish Agarwal] </summary>
|
||||
|
||||
> ### 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]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.48.14 - 2021-09-20
|
||||
|
||||
* os download: Avoid incomplete os downloads appearing as successful [Kyle Harding]
|
||||
|
||||
## 12.48.13 - 2021-09-17
|
||||
|
||||
* config inject: Remove requirement of being logged in [Paulo Castro]
|
||||
|
||||
## 12.48.12 - 2021-09-10
|
||||
|
||||
* build/deploy: Update QEMU to 6.0.0 for emulated builds [Kyle Harding]
|
||||
|
||||
## 12.48.11 - 2021-09-10
|
||||
|
||||
* build, deploy: Fix processing of '--tag' option [Paulo Castro]
|
||||
|
||||
## 12.48.10 - 2021-09-10
|
||||
|
||||
* push: Await and retry supervisor API requests to a local device [Paulo Castro]
|
||||
|
||||
## 12.48.9 - 2021-09-09
|
||||
|
||||
* chore: Update net-keepalive dependency (fix CLI packaging errors) [Paulo Castro]
|
||||
|
||||
## 12.48.8 - 2021-09-08
|
||||
|
||||
* v13 preparations: Add feature switch for removal of '--gitignore' (push, build) [Paulo Castro]
|
||||
* v13 preparations: Adjust test cases for 'balena envs' [Paulo Castro]
|
||||
* v13 preparations: Adjust test cases for 'balena devices' [Paulo Castro]
|
||||
|
||||
## 12.48.7 - 2021-09-07
|
||||
|
||||
* device move: Improve types & reduce the number of API requests [Thodoris Greasidis]
|
||||
* device move: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
|
||||
|
||||
## 12.48.6 - 2021-09-07
|
||||
|
||||
* preload: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
|
||||
|
||||
## 12.48.5 - 2021-09-07
|
||||
|
||||
* preload: Replace my_application query with the SDKs application.getAll() [Thodoris Greasidis]
|
||||
|
||||
## 12.48.4 - 2021-08-31
|
||||
|
||||
* os download: Use the hostApps instead of the device-types/v1 endpoint [Thodoris Greasidis]
|
||||
|
||||
## 12.48.3 - 2021-08-31
|
||||
|
||||
* balena deploy: Retrieve the cpu arch as part of the device type resource [Thodoris Greasidis]
|
||||
|
||||
## 12.48.2 - 2021-08-30
|
||||
|
||||
* Clarify installation instructions [Paulo Castro]
|
||||
|
||||
## 12.48.1 - 2021-08-27
|
||||
|
||||
* Improve error handling (remove most occurrences of process.exit()) [Paulo Castro]
|
||||
* build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe) [Paulo Castro]
|
||||
|
||||
## 12.48.0 - 2021-08-26
|
||||
|
||||
* Add contract contents at release creation time [Paulo Castro]
|
||||
|
@ -105,12 +105,12 @@ npm run update balena-sdk ^13.0.0 major
|
||||
|
||||
## Editing documentation files (README, INSTALL, Reference website...)
|
||||
|
||||
The `doc/cli.markdown` file is automatically generated by running `npm run build:doc` (which also
|
||||
The `docs/balena-cli.md` file is automatically generated by running `npm run build:doc` (which also
|
||||
runs as part of `npm run build`). That file is then pulled by scripts in the
|
||||
[balena-io/docs](https://github.com/balena-io/docs/) GitHub repo for publishing at the [CLI
|
||||
Documentation page](https://www.balena.io/docs/reference/cli/).
|
||||
|
||||
The content sources for the auto generation of `doc/cli.markdown` are:
|
||||
The content sources for the auto generation of `docs/balena-cli.md` are:
|
||||
|
||||
* [Selected
|
||||
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
||||
@ -120,7 +120,7 @@ The content sources for the auto generation of `doc/cli.markdown` are:
|
||||
* `lib/commands/env/add.ts`
|
||||
|
||||
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
||||
`doc/cli.markdown` by the `getCapitanoDoc()` function in
|
||||
`docs/balena-cli.md` by the `getCapitanoDoc()` function in
|
||||
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
||||
|
||||
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
@ -267,4 +267,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
|
||||
|
@ -1,6 +1,10 @@
|
||||
# balena CLI Installation Instructions for Linux
|
||||
|
||||
These instructions are suitable for most Linux distributions on Intel x86, except notably for **Linux Alpine** or **Busybox**. For these distros or for the ARM architecture, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation) method.
|
||||
These instructions are suitable for most Linux distributions on Intel x86, such as
|
||||
Ubuntu, Debian, Fedora, Arch Linux and other glibc-based distributions.
|
||||
For the ARM architecture and for Linux distributions not based on glibc, such as
|
||||
Alpine Linux, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation)
|
||||
method.
|
||||
|
||||
Selected operating system: **Linux**
|
||||
|
||||
|
@ -10,16 +10,14 @@ Selected operating system: **macOS**
|
||||
Look for a file name that ends with "-installer.pkg":
|
||||
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
||||
|
||||
2. Double click the downloaded file to run the installer. After the installation completes,
|
||||
close and re-open any open [command
|
||||
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
|
||||
windows (so that the changes made by the installer to the PATH environment variable can take
|
||||
effect).
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
3. Check that the installation was successful:
|
||||
- [Open the Terminal
|
||||
app](https://support.apple.com/en-gb/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac).
|
||||
- On the terminal prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
|
||||
and `preload` commands may require additional software to be installed, as described
|
||||
|
@ -10,16 +10,14 @@ Selected operating system: **Windows**
|
||||
Look for a file name that ends with "-installer.exe":
|
||||
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
||||
|
||||
2. Double click the downloaded file to run the installer. After the installation completes,
|
||||
close and re-open any open [command
|
||||
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
|
||||
windows (so that the changes made by the installer to the PATH environment variable can take
|
||||
effect).
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
3. Check that the installation was successful:
|
||||
- Click on the Windows Start Menu, type PowerShell, and then click
|
||||
on Windows PowerShell.
|
||||
- On the command prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
|
||||
`deploy` and `preload` commands may require additional software to be installed, as
|
||||
|
@ -144,7 +144,7 @@ To learn more, troubleshoot issues, or to contact us for support:
|
||||
|
||||
* Check the [masterclass tutorials](https://www.balena.io/docs/learn/more/masterclasses/overview/)
|
||||
* Check our [FAQ / troubleshooting document](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md)
|
||||
* Ask us a question through the [balenaCloud forum](https://forums.balena.io/c/balena-cloud)
|
||||
* Ask us a question in the [balena forums](https://forums.balena.io/c/product-support)
|
||||
|
||||
For CLI bug reports or feature requests, check the
|
||||
[CLI GitHub issues](https://github.com/balena-io/balena-cli/issues/).
|
||||
|
@ -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';
|
||||
@ -31,7 +31,7 @@ import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { stripIndent } from '../lib/utils/lazy';
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import {
|
||||
diffLines,
|
||||
loadPackageJson,
|
||||
@ -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'
|
||||
@ -310,7 +311,7 @@ async function zipPkg() {
|
||||
archive.on('warning', console.warn);
|
||||
|
||||
archive.pipe(outputStream);
|
||||
archive.finalize();
|
||||
archive.finalize().catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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',
|
||||
],
|
||||
},
|
||||
@ -82,6 +75,14 @@ const capitanoDoc = {
|
||||
'build/commands/device/shutdown.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Releases',
|
||||
files: [
|
||||
'build/commands/releases.js',
|
||||
'build/commands/release/index.js',
|
||||
'build/commands/release/finalize.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [
|
||||
|
@ -101,8 +101,9 @@ async function printMarkdown() {
|
||||
console.log(await renderMarkdown());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
printMarkdown();
|
||||
|
@ -24,15 +24,15 @@ const simplegit = require('simple-git/promise');
|
||||
const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||
|
||||
/**
|
||||
* Compare the timestamp of cli.markdown with the timestamp of staged files,
|
||||
* issuing an error if cli.markdown is older.
|
||||
* If cli.markdown does not require updating and the developer cannot run
|
||||
* Compare the timestamp of balena-cli.md with the timestamp of staged files,
|
||||
* issuing an error if balena-cli.md is older.
|
||||
* If balena-cli.md does not require updating and the developer cannot run
|
||||
* `npm run build` on their laptop, the error message suggests a workaround
|
||||
* using `touch`.
|
||||
*/
|
||||
async function checkBuildTimestamps() {
|
||||
const git = simplegit(ROOT);
|
||||
const docFile = path.join(ROOT, 'doc', 'cli.markdown');
|
||||
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
||||
const [docStat, gitStatus] = await Promise.all([
|
||||
fs.stat(docFile),
|
||||
git.status(),
|
||||
|
@ -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);
|
||||
@ -41,17 +47,25 @@ function checkNpmVersion() {
|
||||
// the reason is that it would unnecessarily prevent end users from
|
||||
// using npm v6.4.1 that ships with Node 8. (It is OK for the
|
||||
// shrinkwrap file to get damaged if it is not going to be reused.)
|
||||
console.error(`\
|
||||
-------------------------------------------------------------------------------
|
||||
throw new Error(`\
|
||||
-----------------------------------------------------------------------------
|
||||
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
|
||||
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
|
||||
At this point, however, your 'npm-shrinkwrap.json' file has already been
|
||||
damaged. Please revert it to the master branch state with a command such as:
|
||||
"git checkout master -- npm-shrinkwrap.json"
|
||||
Then re-run "npm install" using npm version ${requiredVersion} or later.
|
||||
-------------------------------------------------------------------------------`);
|
||||
process.exit(1);
|
||||
-----------------------------------------------------------------------------`);
|
||||
}
|
||||
}
|
||||
|
||||
checkNpmVersion();
|
||||
function main() {
|
||||
try {
|
||||
checkNpmVersion();
|
||||
} catch (e) {
|
||||
console.error(e.message || e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -54,9 +54,7 @@ export async function release() {
|
||||
try {
|
||||
await createGitHubRelease();
|
||||
} catch (err) {
|
||||
console.error('Release failed');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
throw new Error(`Error creating GitHub release:\n${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,11 +35,6 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
? ''
|
||||
: '1';
|
||||
|
||||
function exitWithError(error: Error | string): never {
|
||||
console.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trivial command-line parser. Check whether the command-line argument is one
|
||||
* of the following strings, then call the appropriate functions:
|
||||
@ -49,12 +44,12 @@ function exitWithError(error: Error | string): never {
|
||||
*
|
||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||
*/
|
||||
export async function run(args?: string[]) {
|
||||
async function parse(args?: string[]) {
|
||||
args = args || process.argv.slice(2);
|
||||
console.log(`automation/run.ts process.argv=[${process.argv}]\n`);
|
||||
console.log(`automation/run.ts args=[${args}]`);
|
||||
console.error(`[debug] automation/run.ts process.argv=[${process.argv}]`);
|
||||
console.error(`[debug] automation/run.ts args=[${args}]`);
|
||||
if (_.isEmpty(args)) {
|
||||
return exitWithError('missing command-line arguments');
|
||||
throw new Error('missing command-line arguments');
|
||||
}
|
||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
@ -66,7 +61,7 @@ export async function run(args?: string[]) {
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (!commands.hasOwnProperty(arg)) {
|
||||
return exitWithError(`command unknown: ${arg}`);
|
||||
throw new Error(`command unknown: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,9 +85,23 @@ export async function run(args?: string[]) {
|
||||
const cmdFunc = commands[arg];
|
||||
await cmdFunc();
|
||||
} catch (err) {
|
||||
return exitWithError(`"${arg}": ${err}`);
|
||||
if (typeof err === 'object') {
|
||||
err.message = `"${arg}": ${err.message}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** See jsdoc for parse() function above */
|
||||
export async function run(args?: string[]) {
|
||||
try {
|
||||
await parse(args);
|
||||
} catch (e) {
|
||||
console.error(e.message ? `Error: ${e.message}` : e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
run();
|
||||
|
@ -11,8 +11,7 @@ const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||
case 'major':
|
||||
return maybeChangeType;
|
||||
default:
|
||||
console.error(`Invalid change type: '${maybeChangeType}'`);
|
||||
return process.exit(1);
|
||||
throw new Error(`Invalid change type: '${maybeChangeType}'`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -37,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);
|
||||
});
|
||||
};
|
||||
|
||||
@ -65,24 +64,17 @@ const getUpstreams = async () => {
|
||||
return upstream;
|
||||
};
|
||||
|
||||
const printUsage = (upstreams: Upstream[], upstreamName: string) => {
|
||||
console.error(
|
||||
`
|
||||
const getUsage = (upstreams: Upstream[], upstreamName: string) => `
|
||||
Usage: npm run update ${upstreamName} $version [$changeType=minor]
|
||||
|
||||
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
|
||||
`,
|
||||
);
|
||||
return process.exit(1);
|
||||
};
|
||||
`;
|
||||
|
||||
// TODO: Drop the wrapper function once we move to TS 3.8,
|
||||
// which will support top level await.
|
||||
async function main() {
|
||||
async function $main() {
|
||||
const upstreams = await getUpstreams();
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
return printUsage(upstreams, '$upstreamName');
|
||||
throw new Error(getUsage(upstreams, '$upstreamName'));
|
||||
}
|
||||
|
||||
const upstreamName = process.argv[2];
|
||||
@ -90,16 +82,15 @@ async function main() {
|
||||
const upstream = upstreams.find((v) => v.repo === upstreamName);
|
||||
|
||||
if (!upstream) {
|
||||
console.error(
|
||||
throw new Error(
|
||||
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
|
||||
.map(({ repo }) => repo)
|
||||
.join(', ')}`,
|
||||
);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
printUsage(upstreams, upstreamName);
|
||||
throw new Error(getUsage(upstreams, upstreamName));
|
||||
}
|
||||
|
||||
const packageName = upstream.module || upstream.repo;
|
||||
@ -108,8 +99,7 @@ async function main() {
|
||||
await run(`npm install ${packageName}@${process.argv[3]}`);
|
||||
const newVersion = await getVersion(packageName);
|
||||
if (newVersion === oldVersion) {
|
||||
console.error(`Already on version '${newVersion}'`);
|
||||
return process.exit(1);
|
||||
throw new Error(`Already on version '${newVersion}'`);
|
||||
}
|
||||
|
||||
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
|
||||
@ -137,4 +127,14 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await $main();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
main();
|
||||
|
@ -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 scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os 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 )
|
||||
@ -21,6 +20,7 @@ _balena() {
|
||||
key_cmds=( add rm )
|
||||
local_cmds=( configure flash )
|
||||
os_cmds=( build-config configure download initialize versions )
|
||||
release_cmds=( finalize )
|
||||
tag_cmds=( rm set )
|
||||
|
||||
|
||||
@ -43,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
|
||||
;;
|
||||
@ -73,6 +70,9 @@ _balena_sec_cmds() {
|
||||
"os")
|
||||
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
|
||||
;;
|
||||
"release")
|
||||
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
|
||||
;;
|
||||
"tag")
|
||||
_describe -t tag_cmds 'tag_cmd' tag_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 scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os 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"
|
||||
@ -20,6 +19,7 @@ _balena_complete()
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
release_cmds="finalize"
|
||||
tag_cmds="rm set"
|
||||
|
||||
|
||||
@ -37,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) )
|
||||
;;
|
||||
@ -67,6 +64,9 @@ _balena_complete()
|
||||
os)
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
release)
|
||||
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
||||
;;
|
||||
tag)
|
||||
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
||||
;;
|
||||
|
@ -1,112 +0,0 @@
|
||||
# Provisioning balena devices in automated (non-interactive) mode
|
||||
|
||||
This document describes how to run the `device init` command in non-interactive mode.
|
||||
|
||||
It requires collecting some preliminary information _once_.
|
||||
|
||||
The final command to provision the device looks like this:
|
||||
|
||||
```bash
|
||||
balena device init --fleet FLEET_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
|
||||
|
||||
```
|
||||
|
||||
You can run this command as many times as you need, putting the new medium (SD card / USB stick) each time.
|
||||
|
||||
But before you can run it you need to collect the parameters and build the configuration file. Keep reading to figure out how to do it.
|
||||
|
||||
|
||||
## Collect all the required parameters.
|
||||
|
||||
1. `DEVICE_TYPE`. Run
|
||||
```bash
|
||||
balena devices supported
|
||||
```
|
||||
and find the _slug_ for your target device type, like _raspberrypi3_.
|
||||
|
||||
1. `FLEET_ID`. Create a fleet (`balena fleet create FLEET_NAME --type DEVICE_TYPE`) or find an existing one (`balena fleets`) and notice its ID.
|
||||
|
||||
1. `OS_VERSION`. Run
|
||||
```bash
|
||||
balena os versions DEVICE_TYPE
|
||||
```
|
||||
and pick the version that you need, like _v2.0.6+rev1.prod_.
|
||||
_Note_ that even though we support _semver ranges_ it's recommended to use the exact version when doing the automated provisioning as it
|
||||
guarantees full compatibility between the steps.
|
||||
|
||||
1. `DRIVE`. Plug in your target medium (SD card or the USB stick, depending on your device type) and run
|
||||
```bash
|
||||
balena util available-drives
|
||||
```
|
||||
and get the drive name, like _/dev/sdb_ or _/dev/mmcblk0_.
|
||||
The balena CLI will not display the system drives to protect you,
|
||||
but still please check very carefully that you've picked the correct drive as it will be erased during the provisioning process.
|
||||
|
||||
Now we have all the parameters -- time to build the config file.
|
||||
|
||||
## Build the config file
|
||||
|
||||
Interactive device provisioning process often includes collecting some extra device configuration, like the networking mode and wifi credentials.
|
||||
|
||||
To skip this interactive step we need to buid this configuration once and save it to the JSON file for later reuse.
|
||||
|
||||
Let's say we will place it into the `CONFIG_FILE` path, like _./balena-os/raspberrypi3-config.json_.
|
||||
|
||||
We also need to put the OS image somewhere, let's call this path `OS_IMAGE_PATH`, it can be something like _./balena-os/raspberrypi3-v2.0.6+rev1.prod.img_.
|
||||
|
||||
1. First we need to download the OS image once. That's needed for building the config, and will speedup the subsequent operations as the downloaded OS image is placed into the local cache.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
balena os download DEVICE_TYPE --output OS_IMAGE_PATH --version OS_VERSION
|
||||
```
|
||||
|
||||
1. Now we're ready to build the config:
|
||||
|
||||
```bash
|
||||
balena os build-config OS_IMAGE_PATH DEVICE_TYPE --output CONFIG_FILE
|
||||
```
|
||||
|
||||
This will run you through the interactive configuration wizard and in the end save the generated config as `CONFIG_FILE`. You can then verify it's not empty:
|
||||
|
||||
```bash
|
||||
cat CONFIG_FILE
|
||||
```
|
||||
|
||||
## Done
|
||||
|
||||
Now you're ready to run the command in the beginning of this guide.
|
||||
|
||||
Please note again that all of these steps only need to be done once (unless you need to change something), and once all the parameters are collected the main init command can be run unchanged.
|
||||
|
||||
But there are still some nuances to cover, please read below.
|
||||
|
||||
## Nuances
|
||||
|
||||
### `sudo` password on *nix systems
|
||||
|
||||
In order to write the image to the raw device we need the root permissions, this is unavoidable.
|
||||
|
||||
To improve the security we only run the minimal subcommand with `sudo`.
|
||||
|
||||
This means that with the default setup you're interrupted closer to the end of the device init process to enter your sudo password for this subcommand to work.
|
||||
|
||||
There are several ways to eliminate it and make the process fully non-interactive.
|
||||
|
||||
#### Option 1: make passwordless sudo.
|
||||
|
||||
Obviously you shouldn't do that if the machine you're working on has access to any sensitive resources or information.
|
||||
|
||||
But if you're using a machine dedicated to balena provisioning this can be fine, and also the simplest thing to do.
|
||||
|
||||
#### Option 2: `NOPASSWD` directive
|
||||
|
||||
You can configure the `balena` CLI command to be sudo-runnable without the password. Check [this post](https://askubuntu.com/questions/159007/how-do-i-run-specific-sudo-commands-without-a-password) for an example.
|
||||
|
||||
### Extra initialization config
|
||||
|
||||
As of June 2017 all the supported devices should not require any other interactive configuration.
|
||||
|
||||
But by the design of our system it is _possible_ (though it doesn't look very likely it's going to happen any time soon) that some extra initialization options may be requested for the specific device types.
|
||||
|
||||
If that is the case please raise the issue in the balena CLI repository and the maintainers will add the necessary options to build the similar JSON config for this step.
|
File diff suppressed because it is too large
Load Diff
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')),
|
||||
);
|
18
lib/app.ts
18
lib/app.ts
@ -33,6 +33,7 @@ export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
autoSessionTracking: false,
|
||||
dsn: config.sentryDsn,
|
||||
release: packageJSON.version,
|
||||
});
|
||||
@ -76,11 +77,13 @@ export function setMaxListeners(maxListeners: number) {
|
||||
/** Selected CLI initialization steps */
|
||||
async function init() {
|
||||
if (process.env.BALENARC_NO_SENTRY) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
}
|
||||
} else {
|
||||
await setupSentry();
|
||||
}
|
||||
checkNodeVersion();
|
||||
await checkNodeVersion();
|
||||
|
||||
const settings = new CliSettings();
|
||||
|
||||
@ -90,14 +93,16 @@ async function init() {
|
||||
setupBalenaSdkSharedOptions(settings);
|
||||
|
||||
// check for CLI updates once a day
|
||||
(await import('./utils/update')).notify();
|
||||
if (!process.env.BALENARC_OFFLINE_MODE) {
|
||||
(await import('./utils/update')).notify();
|
||||
}
|
||||
}
|
||||
|
||||
/** Execute the oclif parser and the CLI command. */
|
||||
async function oclifRun(command: string[], options: AppOptions) {
|
||||
let deprecationPromise: Promise<void>;
|
||||
// check and enforce the CLI's deprecation policy
|
||||
if (unsupportedFlag) {
|
||||
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||
deprecationPromise = Promise.resolve();
|
||||
} else {
|
||||
const { DeprecationChecker } = await import('./deprecation');
|
||||
@ -148,7 +153,10 @@ async function oclifRun(command: string[], options: AppOptions) {
|
||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||
export async function run(cliArgs = process.argv, options: AppOptions = {}) {
|
||||
try {
|
||||
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
|
||||
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
||||
'./utils/bootstrap'
|
||||
);
|
||||
setOfflineModeEnvVars();
|
||||
normalizeEnvVars();
|
||||
|
||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
||||
|
@ -56,7 +56,7 @@ export async function login({ host = '127.0.0.1', port = 0 }) {
|
||||
|
||||
console.info(`Opening web browser for URL:\n${loginUrl}`);
|
||||
const open = await import('open');
|
||||
open(loginUrl, { wait: false });
|
||||
await open(loginUrl, { wait: false });
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const token = await loginServer.awaitForToken();
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -16,7 +16,12 @@
|
||||
*/
|
||||
|
||||
import Command from '@oclif/command';
|
||||
import { InsufficientPrivilegesError } from './errors';
|
||||
import {
|
||||
InsufficientPrivilegesError,
|
||||
NotAvailableInOfflineModeError,
|
||||
} from './errors';
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import * as output from './framework/output';
|
||||
|
||||
export default abstract class BalenaCommand extends Command {
|
||||
/**
|
||||
@ -40,6 +45,13 @@ export default abstract class BalenaCommand extends Command {
|
||||
*/
|
||||
public static authenticated = false;
|
||||
|
||||
/**
|
||||
* Require an internet connection to run.
|
||||
* When set to true, command will exit with an error
|
||||
* if user is running in offline mode (BALENARC_OFFLINE_MODE).
|
||||
*/
|
||||
public static offlineCompatible = false;
|
||||
|
||||
/**
|
||||
* Accept piped input.
|
||||
* When set to true, command will read from stdin during init
|
||||
@ -97,6 +109,29 @@ export default abstract class BalenaCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw NotAvailableInOfflineModeError if in offline mode.
|
||||
*
|
||||
* Called automatically if `onlineOnly=true`.
|
||||
* Can be called explicitly by command implementation, if e.g.:
|
||||
* - check should only be done conditionally
|
||||
* - other code needs to execute before check
|
||||
*
|
||||
* Note, currently public to allow use outside of derived commands
|
||||
* (as some command implementations require this. Can be made protected
|
||||
* if this changes).
|
||||
*
|
||||
* @throws {NotAvailableInOfflineModeError}
|
||||
*/
|
||||
public static checkNotUsingOfflineMode() {
|
||||
if (process.env.BALENARC_OFFLINE_MODE) {
|
||||
throw new NotAvailableInOfflineModeError(stripIndent`
|
||||
This command requires an internet connection, and cannot be used in offline mode.
|
||||
To leave offline mode, unset the BALENARC_OFFLINE_MODE environment variable.
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read stdin contents and make available to command.
|
||||
*
|
||||
@ -125,8 +160,16 @@ export default abstract class BalenaCommand extends Command {
|
||||
await BalenaCommand.checkLoggedIn();
|
||||
}
|
||||
|
||||
if (!ctr.offlineCompatible) {
|
||||
BalenaCommand.checkNotUsingOfflineMode();
|
||||
}
|
||||
|
||||
if (ctr.readStdin) {
|
||||
await this.getStdin();
|
||||
}
|
||||
}
|
||||
|
||||
protected outputMessage = output.outputMessage;
|
||||
protected outputData = output.outputData;
|
||||
protected printTitle = output.printTitle;
|
||||
}
|
||||
|
@ -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 ${params.fleet} 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;
|
||||
@ -239,7 +227,12 @@ ${dockerignoreHelp}
|
||||
) {
|
||||
const { loadProject } = await import('../utils/compose_ts');
|
||||
|
||||
const project = await loadProject(logger, composeOpts);
|
||||
const project = await loadProject(
|
||||
logger,
|
||||
composeOpts,
|
||||
undefined,
|
||||
opts.buildOpts.t,
|
||||
);
|
||||
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
if (
|
||||
@ -266,7 +259,6 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
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;
|
||||
@ -42,6 +36,7 @@ interface FlagsDef {
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
appUpdatePollInterval?: string;
|
||||
'provisioning-key-name'?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -53,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.
|
||||
|
||||
@ -65,13 +62,12 @@ export default class ConfigGenerateCmd extends Command {
|
||||
public static examples = [
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>',
|
||||
'$ 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';
|
||||
@ -81,20 +77,12 @@ 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'] },
|
||||
device: { ...cf.device, exclusive: ['application', 'app', 'fleet'] },
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
dev: cf.dev,
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['fleet', 'provisioning-key-name'],
|
||||
},
|
||||
deviceApiKey: flags.string({
|
||||
description:
|
||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||
@ -126,7 +114,11 @@ 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',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
@ -165,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' },
|
||||
},
|
||||
@ -180,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,
|
||||
);
|
||||
@ -190,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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -199,9 +191,11 @@ 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
|
||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||
@ -244,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);
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
type?: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
}
|
||||
@ -32,15 +32,18 @@ interface ArgsDef {
|
||||
|
||||
export default class ConfigInjectCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Inject a configuration file into a device or OS image.
|
||||
Inject a config.json file to a balenaOS image or attached media.
|
||||
|
||||
Inject a config.json file to the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
Inject a 'config.json' file to a balenaOS image file or attached SD card or
|
||||
USB stick.
|
||||
|
||||
Documentation for the balenaOS 'config.json' file can be found at:
|
||||
https://www.balena.io/docs/reference/OS/configuration/
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config inject my/config.json --type raspberrypi3',
|
||||
'$ balena config inject my/config.json --type raspberrypi3 --drive /dev/disk2',
|
||||
'$ balena config inject my/config.json',
|
||||
'$ balena config inject my/config.json --drive /dev/disk2',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -54,14 +57,13 @@ export default class ConfigInjectCmd extends Command {
|
||||
public static usage = 'config inject <file>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: cf.deviceType,
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
@ -80,7 +82,7 @@ export default class ConfigInjectCmd extends Command {
|
||||
);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
await config.write(drive, options.type, configJSON);
|
||||
await config.write(drive, '', configJSON);
|
||||
|
||||
console.info('Done');
|
||||
}
|
||||
|
@ -21,35 +21,40 @@ import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
type?: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
export default class ConfigReadCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Read the configuration of a device or OS image.
|
||||
Read the config.json file of a balenaOS image or attached media.
|
||||
|
||||
Read the config.json file from the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
Read the 'config.json' file of a balenaOS image file or attached SD card or
|
||||
USB stick.
|
||||
|
||||
Documentation for the balenaOS 'config.json' file can be found at:
|
||||
https://www.balena.io/docs/reference/OS/configuration/
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config read --type raspberrypi3',
|
||||
'$ balena config read --type raspberrypi3 --drive /dev/disk2',
|
||||
'$ balena config read',
|
||||
'$ balena config read --drive /dev/disk2',
|
||||
'$ balena config read --drive balena.img',
|
||||
];
|
||||
|
||||
public static usage = 'config read';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: cf.deviceType,
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
|
||||
@ -61,9 +66,13 @@ export default class ConfigReadCmd extends Command {
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const configJSON = await config.read(drive, options.type);
|
||||
const configJSON = await config.read(drive, '');
|
||||
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.info(prettyjson.render(configJSON));
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(configJSON, null, 4));
|
||||
} else {
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(configJSON));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,38 +21,48 @@ import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
type?: string;
|
||||
drive?: string;
|
||||
advanced: boolean;
|
||||
help: void;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export default class ConfigReconfigureCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Interactively reconfigure a device or OS image.
|
||||
Interactively reconfigure a balenaOS image file or attached media.
|
||||
|
||||
Interactively reconfigure a provisioned device or OS image.
|
||||
Interactively reconfigure a balenaOS image file or attached media.
|
||||
|
||||
This command extracts the device UUID from the 'config.json' file of the
|
||||
chosen balenaOS image file or attached media, and then passes the UUID as
|
||||
the '--device' argument to the 'balena os configure' command.
|
||||
|
||||
For finer-grained or scripted control of the operation, use the
|
||||
'balena config read' and 'balena os configure' commands separately.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena config reconfigure --type raspberrypi3',
|
||||
'$ balena config reconfigure --type raspberrypi3 --advanced',
|
||||
'$ balena config reconfigure --type raspberrypi3 --drive /dev/disk2',
|
||||
'$ balena config reconfigure',
|
||||
'$ balena config reconfigure --drive /dev/disk3',
|
||||
'$ balena config reconfigure --drive balena.img --advanced',
|
||||
];
|
||||
|
||||
public static usage = 'config reconfigure';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: cf.deviceType,
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
advanced: flags.boolean({
|
||||
description: 'show advanced commands',
|
||||
char: 'v',
|
||||
}),
|
||||
help: cf.help,
|
||||
version: flags.string({
|
||||
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
@ -65,10 +75,20 @@ export default class ConfigReconfigureCmd extends Command {
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const { uuid } = await config.read(drive, options.type);
|
||||
const { uuid } = await config.read(drive, '');
|
||||
await safeUmount(drive);
|
||||
|
||||
if (!uuid) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(
|
||||
`Error: UUID not found in 'config.json' file for '${drive}'`,
|
||||
);
|
||||
}
|
||||
|
||||
const configureCommand = ['os', 'configure', drive, '--device', uuid];
|
||||
if (options.version) {
|
||||
configureCommand.push('--version', options.version);
|
||||
}
|
||||
if (options.advanced) {
|
||||
configureCommand.push('--advanced');
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
type?: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
}
|
||||
@ -33,16 +33,19 @@ interface ArgsDef {
|
||||
|
||||
export default class ConfigWriteCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Write a key-value pair to configuration of a device or OS image.
|
||||
Write a key-value pair to the config.json file of an OS image or attached media.
|
||||
|
||||
Write a key-value pair to the config.json file on the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
Write a key-value pair to the 'config.json' file of a balenaOS image file or
|
||||
attached SD card or USB stick.
|
||||
|
||||
Documentation for the balenaOS 'config.json' file can be found at:
|
||||
https://www.balena.io/docs/reference/OS/configuration/
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config write --type raspberrypi3 username johndoe',
|
||||
'$ balena config write --type raspberrypi3 --drive /dev/disk2 username johndoe',
|
||||
'$ balena config write --type raspberrypi3 files.network/settings "..."',
|
||||
'$ balena config write ntpServers "0.resinio.pool.ntp.org 1.resinio.pool.ntp.org"',
|
||||
'$ balena config write --drive /dev/disk2 hostname custom-hostname',
|
||||
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -61,14 +64,13 @@ export default class ConfigWriteCmd extends Command {
|
||||
public static usage = 'config write <key> <value>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: cf.deviceType,
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
@ -82,14 +84,14 @@ export default class ConfigWriteCmd extends Command {
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const configJSON = await config.read(drive, options.type);
|
||||
const configJSON = await config.read(drive, '');
|
||||
|
||||
console.info(`Setting ${params.key} to ${params.value}`);
|
||||
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
|
||||
|
||||
await denyMount(drive, async () => {
|
||||
await safeUmount(drive);
|
||||
await config.write(drive, options.type, configJSON);
|
||||
await config.write(drive, '', configJSON);
|
||||
});
|
||||
|
||||
console.info('Done');
|
||||
|
@ -34,7 +34,7 @@ import type {
|
||||
ComposeOpts,
|
||||
Release as ComposeReleaseInfo,
|
||||
} from '../utils/compose-types';
|
||||
import type { DockerCliFlags } from '../utils/docker';
|
||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||
import {
|
||||
applyReleaseTagKeysAndValues,
|
||||
buildProject,
|
||||
@ -245,7 +245,7 @@ ${dockerignoreHelp}
|
||||
shouldPerformBuild: boolean;
|
||||
shouldUploadLogs: boolean;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: any; // arguments to forward to docker build command
|
||||
buildOpts: BuildOpts;
|
||||
createAsDraft: boolean;
|
||||
},
|
||||
) {
|
||||
@ -259,7 +259,12 @@ ${dockerignoreHelp}
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
|
||||
try {
|
||||
const project = await loadProject(logger, composeOpts, opts.image);
|
||||
const project = await loadProject(
|
||||
logger,
|
||||
composeOpts,
|
||||
opts.image,
|
||||
opts.buildOpts.t,
|
||||
);
|
||||
if (project.descriptors.length > 1 && !appType?.supports_multicontainer) {
|
||||
throw new ExpectedError(
|
||||
'Target fleet does not support multiple containers. Aborting!',
|
||||
@ -314,7 +319,6 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,15 +21,15 @@ 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';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
application_name?: string;
|
||||
fleet: string; // 'org/name' slug
|
||||
device_type?: string;
|
||||
commit?: string;
|
||||
last_seen?: string;
|
||||
@ -44,9 +44,8 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
undervoltage_detected?: boolean;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
v13: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -74,7 +73,7 @@ export default class DeviceCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
v13: cf.v13,
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
@ -84,7 +83,6 @@ export default class DeviceCmd extends Command {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceCmd,
|
||||
);
|
||||
const useAppWord = !options.v13 && !isV13();
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
@ -121,8 +119,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,41 +168,52 @@ export default class DeviceCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
if (useAppWord && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetOutputMsg));
|
||||
}
|
||||
const outputFields = [
|
||||
'device_name',
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
'fleet',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
'cpu_usage_percent',
|
||||
'cpu_temp_c',
|
||||
'cpu_id',
|
||||
'memory_usage_mb',
|
||||
'memory_total_mb',
|
||||
'memory_usage_percent',
|
||||
'storage_block_device',
|
||||
'storage_usage_mb',
|
||||
'storage_total_mb',
|
||||
'storage_usage_percent',
|
||||
'undervoltage_detected',
|
||||
];
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
useAppWord ? 'application_name' : 'application_name => FLEET',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
'cpu_usage_percent',
|
||||
'cpu_temp_c',
|
||||
'cpu_id',
|
||||
'memory_usage_mb',
|
||||
'memory_total_mb',
|
||||
'memory_usage_percent',
|
||||
'storage_block_device',
|
||||
'storage_usage_mb',
|
||||
'storage_total_mb',
|
||||
'storage_usage_percent',
|
||||
'undervoltage_detected',
|
||||
]),
|
||||
);
|
||||
if (isV14()) {
|
||||
await this.outputData(device, outputFields, {
|
||||
...options,
|
||||
hideNullOrUndefinedValues: true,
|
||||
titleField: 'device_name',
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
outputFields.unshift(`$${device.device_name}$`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(
|
||||
device,
|
||||
outputFields.filter((f) => f !== 'device_name'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -37,35 +30,51 @@ interface FlagsDef {
|
||||
drive?: string;
|
||||
config?: string;
|
||||
help: void;
|
||||
'provisioning-key-name'?: string;
|
||||
}
|
||||
|
||||
export default class DeviceInitCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Initialize a device with balenaOS.
|
||||
|
||||
Initialize a device by downloading the OS image of the specified fleet
|
||||
and writing it to an SD Card.
|
||||
Register a new device in the selected fleet, download the OS image for the
|
||||
fleet's default device type, configure the image and write it to an SD card.
|
||||
This command effectively combines several other balena CLI commands in one,
|
||||
namely:
|
||||
|
||||
If the --fleet option is omitted, it will be prompted for interactively.
|
||||
'balena device register'
|
||||
'balena os download'
|
||||
'balena os build-config' or 'balena config generate'
|
||||
'balena os configure'
|
||||
'balena os local flash'
|
||||
|
||||
Possible arguments for the '--fleet', '--os-version' and '--drive' options can
|
||||
be listed respectively with the commands:
|
||||
|
||||
'balena fleets'
|
||||
'balena os versions'
|
||||
'balena util available-drives'
|
||||
|
||||
If the '--fleet' or '--drive' options are omitted, interactive menus will be
|
||||
presented with values to choose from. If the '--os-version' option is omitted,
|
||||
the latest released OS version for the fleet's default device type will be used.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
Image configuration questions will be asked interactively unless a pre-configured
|
||||
'config.json' file is provided with the '--config' option. The file can be
|
||||
generated with the 'balena config generate' or 'balena os build-config' commands.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena device init',
|
||||
'$ balena device init --fleet MyFleet',
|
||||
'$ balena device init -f myorg/myfleet',
|
||||
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes',
|
||||
];
|
||||
|
||||
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({
|
||||
@ -85,6 +94,9 @@ export default class DeviceInitCmd extends Command {
|
||||
config: flags.string({
|
||||
description: 'path to the config JSON file, see `balena os build-config`',
|
||||
}),
|
||||
'provisioning-key-name': flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -105,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,
|
||||
@ -130,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);
|
||||
|
||||
@ -173,6 +178,13 @@ export default class DeviceInitCmd extends Command {
|
||||
} else if (options.advanced) {
|
||||
configureCommand.push('--advanced');
|
||||
}
|
||||
|
||||
if (options['provisioning-key-name']) {
|
||||
configureCommand.push(
|
||||
'--provisioning-key-name',
|
||||
options['provisioning-key-name'],
|
||||
);
|
||||
}
|
||||
await runCommand(configureCommand);
|
||||
}
|
||||
|
||||
|
@ -17,25 +17,26 @@
|
||||
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import type { Application, BalenaSDK } from 'balena-sdk';
|
||||
import type {
|
||||
BalenaSDK,
|
||||
Device,
|
||||
DeviceType,
|
||||
PineTypedResult,
|
||||
} from 'balena-sdk';
|
||||
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';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
type ExtendedDevice = PineTypedResult<
|
||||
Device,
|
||||
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
|
||||
> & {
|
||||
application_name?: string;
|
||||
}
|
||||
};
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
help: void;
|
||||
}
|
||||
@ -74,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,
|
||||
};
|
||||
@ -86,15 +86,10 @@ 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');
|
||||
const { expandForAppName } = await import('../../utils/helpers');
|
||||
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
|
||||
|
||||
// Parse ids string into array of correct types
|
||||
const deviceIds: Array<string | number> = params.uuid
|
||||
@ -107,15 +102,14 @@ export default class DeviceMoveCmd extends Command {
|
||||
(uuid) =>
|
||||
balena.models.device.get(
|
||||
uuid,
|
||||
expandForAppName,
|
||||
expandForAppNameAndCpuArch,
|
||||
) as Promise<ExtendedDevice>,
|
||||
),
|
||||
);
|
||||
|
||||
// Map application name for each device
|
||||
for (const device of devices) {
|
||||
const belongsToApplication =
|
||||
device.belongs_to__application as Application[];
|
||||
const belongsToApplication = device.belongs_to__application;
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
@ -125,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
|
||||
@ -145,42 +139,56 @@ export default class DeviceMoveCmd extends Command {
|
||||
balena: BalenaSDK,
|
||||
devices: ExtendedDevice[],
|
||||
) {
|
||||
const [deviceDeviceTypes, deviceTypes] = await Promise.all([
|
||||
Promise.all(
|
||||
devices.map((device) =>
|
||||
balena.models.device.getManifestBySlug(
|
||||
device.is_of__device_type[0].slug,
|
||||
),
|
||||
const { getExpandedProp } = await import('../../utils/pine');
|
||||
// deduplicate the slugs
|
||||
const deviceCpuArchs = Array.from(
|
||||
new Set(
|
||||
devices.map(
|
||||
(d) => d.is_of__device_type[0].is_of__cpu_architecture[0].slug,
|
||||
),
|
||||
),
|
||||
balena.models.config.getDeviceTypes(),
|
||||
]);
|
||||
);
|
||||
|
||||
const compatibleDeviceTypes = deviceTypes.filter((dt) =>
|
||||
deviceDeviceTypes.every(
|
||||
(deviceDeviceType) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceDeviceType.arch,
|
||||
dt.arch,
|
||||
) &&
|
||||
!!dt.isDependent === !!deviceDeviceType.isDependent &&
|
||||
dt.state !== 'DISCONTINUED',
|
||||
),
|
||||
const deviceTypeOptions = {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
const deviceTypes = (await balena.models.deviceType.getAllSupported(
|
||||
deviceTypeOptions,
|
||||
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
|
||||
|
||||
const compatibleDeviceTypeSlugs = new Set(
|
||||
deviceTypes
|
||||
.filter((deviceType) => {
|
||||
const deviceTypeArch = getExpandedProp(
|
||||
deviceType.is_of__cpu_architecture,
|
||||
'slug',
|
||||
)!;
|
||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceCpuArch,
|
||||
deviceTypeArch,
|
||||
),
|
||||
);
|
||||
})
|
||||
.map((deviceType) => deviceType.slug),
|
||||
);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
try {
|
||||
const application = await patterns.selectApplication(
|
||||
(app) =>
|
||||
compatibleDeviceTypes.some(
|
||||
(dt) => dt.slug === app.is_for__device_type[0].slug,
|
||||
) &&
|
||||
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
|
||||
devices.some((device) => device.application_name !== app.app_name),
|
||||
true,
|
||||
);
|
||||
return application;
|
||||
} catch (err) {
|
||||
if (deviceDeviceTypes.length) {
|
||||
if (!compatibleDeviceTypeSlugs.size) {
|
||||
throw new ExpectedError(
|
||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||
);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -20,30 +20,22 @@ 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';
|
||||
import type { DataSetOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
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;
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
v13: boolean;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export default class DevicesCmd extends Command {
|
||||
@ -67,50 +59,24 @@ 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'] },
|
||||
json: cf.json,
|
||||
fleet: cf.fleet,
|
||||
...(isV14() ? cf.dataSetOutputFlags : { 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 +92,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,38 +100,52 @@ 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}`,
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
if (isV14()) {
|
||||
const outputFields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
await this.outputData(devices, outputFields, {
|
||||
...options,
|
||||
displayNullValuesAs: 'N/a',
|
||||
});
|
||||
} else {
|
||||
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetOutputMsg));
|
||||
// Old output implementation
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -17,37 +17,24 @@
|
||||
import { flags } from '@oclif/command';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import type { DataSetOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
discontinued: boolean;
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
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 +43,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 +52,8 @@ 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)',
|
||||
}),
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
@ -95,60 +67,41 @@ 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[];
|
||||
aliases: string[] | 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',
|
||||
aliases: options.json ? aliases : aliases.join(', '),
|
||||
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
|
||||
name: dt.name || 'N/A',
|
||||
});
|
||||
}
|
||||
const fields =
|
||||
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));
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(deviceTypes, fields, options);
|
||||
} else {
|
||||
const visuals = getVisuals();
|
||||
const output = await visuals.table.horizontal(deviceTypes, fields);
|
||||
console.log(output);
|
||||
// Old output implementation
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
} else {
|
||||
const visuals = getVisuals();
|
||||
const output = await visuals.table.horizontal(deviceTypes, fields);
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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);
|
||||
|
2
lib/commands/env/rm.ts
vendored
2
lib/commands/env/rm.ts
vendored
@ -91,8 +91,6 @@ export default class EnvRmCmd extends Command {
|
||||
await confirm(
|
||||
opt.yes || false,
|
||||
'Are you sure you want to delete the environment variable?',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,42 +21,36 @@ 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';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
config: boolean;
|
||||
device?: string; // device UUID
|
||||
json: boolean;
|
||||
json?: boolean;
|
||||
help: void;
|
||||
service?: string; // service name
|
||||
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 +90,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 +107,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,
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
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 +148,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,45 +176,67 @@ 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');
|
||||
}
|
||||
if (!options.config) {
|
||||
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
|
||||
}
|
||||
if (isV14()) {
|
||||
const results = [...varArray] as any;
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../utils/helpers');
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetOutputMsg));
|
||||
// Rename fields
|
||||
if (options.device) {
|
||||
if (options.json) {
|
||||
fields.push('deviceUUID');
|
||||
} else {
|
||||
results.forEach((r: any) => {
|
||||
r.device = r.deviceUUID;
|
||||
delete r.deviceUUID;
|
||||
});
|
||||
|
||||
fields.push('device');
|
||||
}
|
||||
}
|
||||
if (!options.config) {
|
||||
if (options.json) {
|
||||
fields.push('serviceName');
|
||||
} else {
|
||||
results.forEach((r: any) => {
|
||||
r.service = r.serviceName;
|
||||
delete r.serviceName;
|
||||
});
|
||||
fields.push('service');
|
||||
}
|
||||
}
|
||||
|
||||
await this.outputData(results, fields, {
|
||||
...options,
|
||||
sort: options.sort || 'name',
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
if (!options.config) {
|
||||
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../utils/helpers');
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -252,14 +244,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 +265,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 +291,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 +307,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 +316,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 +337,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 +359,7 @@ function fillInInfoFields(
|
||||
| EnvironmentVariableInfo[]
|
||||
| DeviceServiceEnvironmentVariableInfo[]
|
||||
| ServiceEnvironmentVariableInfo[],
|
||||
appNameOrSlug?: string,
|
||||
fleetSlug?: string,
|
||||
deviceUUID?: string,
|
||||
) {
|
||||
for (const envVar of varArray) {
|
||||
@ -381,7 +373,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',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ export default class OsinitCmd extends Command {
|
||||
|
||||
public static hidden = true;
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -20,10 +20,13 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -52,27 +55,52 @@ export default class KeyCmd extends Command {
|
||||
public static usage = 'key <id>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
KeyCmd,
|
||||
);
|
||||
|
||||
const key = await getBalenaSdk().models.key.get(params.id);
|
||||
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
};
|
||||
if (isV14()) {
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
public_key: key.public_key,
|
||||
};
|
||||
|
||||
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
|
||||
if (!options.json) {
|
||||
// Id is redundant, since user must have provided it in command call
|
||||
this.printTitle(displayKey.name);
|
||||
this.outputMessage(displayKey.public_key);
|
||||
} else {
|
||||
await this.outputData(
|
||||
displayKey,
|
||||
['id', 'name', 'public_key'],
|
||||
options,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Old output implementation
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
};
|
||||
|
||||
// Since the public key string is long, it might
|
||||
// wrap to lines below, causing the table layout to break.
|
||||
// See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key);
|
||||
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
|
||||
|
||||
// Since the public key string is long, it might
|
||||
// wrap to lines below, causing the table layout to break.
|
||||
// See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2022 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -35,13 +38,14 @@ export default class KeysCmd extends Command {
|
||||
public static usage = 'keys';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(KeysCmd);
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(KeysCmd);
|
||||
|
||||
const keys = await getBalenaSdk().models.key.getAll();
|
||||
|
||||
@ -50,6 +54,12 @@ export default class KeysCmd extends Command {
|
||||
return { id: k.id, name: k.title };
|
||||
});
|
||||
|
||||
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
|
||||
// Display
|
||||
if (isV14()) {
|
||||
await this.outputData(displayKeys, ['id', 'name'], options);
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ export default class LocalConfigureCmd extends Command {
|
||||
};
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
|
||||
@ -96,7 +97,7 @@ export default class LocalConfigureCmd extends Command {
|
||||
|
||||
readonly CONNECTIONS_FOLDER = '/system-connections';
|
||||
|
||||
getConfigurationSchema(bootPartition: number, connectionFileName?: string) {
|
||||
getConfigurationSchema(bootPartition?: number, connectionFileName?: string) {
|
||||
connectionFileName ??= 'resin-wifi';
|
||||
return {
|
||||
mapper: [
|
||||
@ -112,6 +113,12 @@ export default class LocalConfigureCmd extends Command {
|
||||
},
|
||||
domain: [['config_json', 'hostname']],
|
||||
},
|
||||
{
|
||||
template: {
|
||||
developmentMode: '{{developmentMode}}',
|
||||
},
|
||||
domain: [['config_json', 'developmentMode']],
|
||||
},
|
||||
{
|
||||
template: {
|
||||
wifi: {
|
||||
@ -162,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',
|
||||
@ -235,9 +249,9 @@ export default class LocalConfigureCmd extends Command {
|
||||
async prepareConnectionFile(target: string) {
|
||||
const _ = await import('lodash');
|
||||
const imagefs = await import('balena-image-fs');
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const { getBootPartition } = await import('balena-config-json');
|
||||
|
||||
const bootPartition = await helpers.getBootPartition(target);
|
||||
const bootPartition = await getBootPartition(target);
|
||||
|
||||
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
|
||||
|
@ -20,12 +20,7 @@ import type { BlockDevice } from 'etcher-sdk/build/source-destination';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import {
|
||||
getChalk,
|
||||
getCliForm,
|
||||
getVisuals,
|
||||
stripIndent,
|
||||
} from '../../utils/lazy';
|
||||
import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
@ -70,6 +65,8 @@ export default class LocalFlashCmd extends Command {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
LocalFlashCmd,
|
||||
@ -93,24 +90,15 @@ export default class LocalFlashCmd extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||
|
||||
const drive = await this.getDrive(options);
|
||||
|
||||
const yes =
|
||||
options.yes ||
|
||||
(await getCliForm().ask({
|
||||
message: 'This will erase the selected drive. Are you sure?',
|
||||
type: 'confirm',
|
||||
name: 'yes',
|
||||
default: false,
|
||||
}));
|
||||
|
||||
if (!yes) {
|
||||
console.log(getChalk().red.bold('Aborted image flash'));
|
||||
process.exit(0);
|
||||
}
|
||||
const { confirm } = await import('../../utils/patterns');
|
||||
await confirm(
|
||||
options.yes,
|
||||
'This will erase the selected drive. Are you sure?',
|
||||
);
|
||||
|
||||
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||
const file = new sourceDestination.File({
|
||||
path: params.image,
|
||||
});
|
||||
|
@ -187,7 +187,7 @@ ${messages.reachingOut}`);
|
||||
if (loginType === 'register') {
|
||||
const open = await import('open');
|
||||
const signupUrl = `https://dashboard.${balenaUrl}/signup`;
|
||||
open(signupUrl, { wait: false });
|
||||
await open(signupUrl, { wait: false });
|
||||
throw new ExpectedError(`Please sign up at ${signupUrl}`);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2022 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -36,12 +39,13 @@ export default class OrgsCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(OrgsCmd);
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(OrgsCmd);
|
||||
|
||||
const { getOwnOrganizations } = await import('../utils/sdk');
|
||||
|
||||
@ -49,8 +53,13 @@ export default class OrgsCmd extends Command {
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk());
|
||||
|
||||
// Display
|
||||
console.log(
|
||||
getVisuals().table.horizontal(organizations, ['name', 'handle']),
|
||||
);
|
||||
if (isV14()) {
|
||||
await this.outputData(organizations, ['name', 'handle'], options);
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(
|
||||
getVisuals().table.horizontal(organizations, ['name', 'handle']),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,32 +23,26 @@ 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;
|
||||
'system-connection': string[];
|
||||
'initial-device-name'?: string;
|
||||
'provisioning-key-name'?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -57,17 +51,15 @@ 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"
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
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.
|
||||
@ -81,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
|
||||
@ -103,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',
|
||||
@ -126,25 +117,11 @@ 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',
|
||||
exclusive: ['provisioning-key-name'],
|
||||
}),
|
||||
'config-app-update-poll-interval': flags.integer({
|
||||
description:
|
||||
@ -160,12 +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'], ...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',
|
||||
@ -184,17 +157,19 @@ export default class OsConfigureCmd extends Command {
|
||||
description:
|
||||
"paths to local files to place into the 'system-connections' directory",
|
||||
}),
|
||||
'provisioning-key-name': flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
exclusive: ['config', 'device'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
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);
|
||||
|
||||
@ -221,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' },
|
||||
},
|
||||
@ -242,25 +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);
|
||||
}
|
||||
@ -300,7 +278,8 @@ export default class OsConfigureCmd extends Command {
|
||||
}),
|
||||
);
|
||||
|
||||
const bootPartition = await helpers.getBootPartition(params.image);
|
||||
const { getBootPartition } = await import('balena-config-json');
|
||||
const bootPartition = await getBootPartition(params.image);
|
||||
|
||||
const imagefs = await import('balena-image-fs');
|
||||
|
||||
@ -320,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();
|
||||
}
|
||||
@ -386,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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -411,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;
|
||||
|
@ -34,30 +34,33 @@ export default class OsDownloadCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Download an unconfigured OS image.
|
||||
|
||||
Download an unconfigured OS image for a certain device type.
|
||||
Check available types with \`balena devices supported\`
|
||||
Download an unconfigured OS image for the specified device type.
|
||||
Check available device types with 'balena devices supported'.
|
||||
|
||||
Note: Currently this command only works with balenaCloud, not openBalena.
|
||||
If using openBalena, please download the OS from: https://www.balena.io/os/
|
||||
|
||||
If version is not specified the newest stable (non-pre-release) version of OS
|
||||
is downloaded (if available), otherwise the newest version (if all existing
|
||||
versions for the given device type are pre-release).
|
||||
The '--version' option is used to select the balenaOS version. If omitted,
|
||||
the latest released version is downloaded (and if only pre-release versions
|
||||
exist, the latest pre-release version is downloaded).
|
||||
|
||||
You can pass \`--version menu\` to pick the OS version from the interactive menu
|
||||
of all available versions.
|
||||
Use '--version menu' or '--version menu-esr' to interactively select the
|
||||
OS version. The latter lists ESR versions which are only available for
|
||||
download on Production and Enterprise plans. See also:
|
||||
https://www.balena.io/docs/reference/OS/extended-support-release/
|
||||
|
||||
To download a development image append \`.dev\` to the version or select from
|
||||
the interactive menu.
|
||||
Development images can be selected by appending \`.dev\` to the version.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img',
|
||||
'$ 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 = [
|
||||
@ -78,11 +81,13 @@ export default class OsDownloadCmd extends Command {
|
||||
}),
|
||||
version: flags.string({
|
||||
description: stripIndent`
|
||||
exact version number, or a valid semver range,
|
||||
version number (ESR or non-ESR versions),
|
||||
or semver range (non-ESR versions only),
|
||||
or 'latest' (includes pre-releases),
|
||||
or 'default' (excludes pre-releases if at least one stable version is available),
|
||||
or 'default' (excludes pre-releases if at least one released version is available),
|
||||
or 'recommended' (excludes pre-releases, will fail if only pre-release versions are available),
|
||||
or 'menu' (will show the interactive menu)
|
||||
or 'menu' (interactive menu, non-ESR versions),
|
||||
or 'menu-esr' (interactive menu, ESR versions)
|
||||
`,
|
||||
}),
|
||||
help: cf.help,
|
||||
@ -93,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;
|
||||
}
|
||||
}
|
||||
|
@ -92,7 +92,6 @@ export default class OsInitializeCmd extends Command {
|
||||
options.yes,
|
||||
`This will erase ${answers.drive}. Are you sure?`,
|
||||
`Going to erase ${answers.drive}.`,
|
||||
true,
|
||||
);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
await safeUmount(answers.drive);
|
||||
|
@ -18,9 +18,10 @@
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
esr?: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -34,6 +35,9 @@ export default class OsVersionsCmd extends Command {
|
||||
|
||||
Show the available balenaOS versions for the given device type.
|
||||
Check available types with \`balena devices supported\`.
|
||||
|
||||
balenaOS ESR versions can be listed with the '--esr' option. See also:
|
||||
https://www.balena.io/docs/reference/OS/extended-support-release/
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena os versions raspberrypi3'];
|
||||
@ -50,16 +54,22 @@ export default class OsVersionsCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
esr: flags.boolean({
|
||||
description: 'select balenaOS ESR versions',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(OsVersionsCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
OsVersionsCmd,
|
||||
);
|
||||
|
||||
const { versions: vs, recommended } =
|
||||
await getBalenaSdk().models.os.getSupportedVersions(params.type);
|
||||
const { formatOsVersion, getOsVersions } = await import(
|
||||
'../../utils/cloud'
|
||||
);
|
||||
const vs = await getOsVersions(params.type, !!options.esr);
|
||||
|
||||
vs.forEach((v) => {
|
||||
console.log(`v${v}` + (v === recommended ? ' (recommended)' : ''));
|
||||
});
|
||||
console.log(vs.map((v) => formatOsVersion(v)).join('\n'));
|
||||
}
|
||||
}
|
||||
|
@ -24,29 +24,17 @@ 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';
|
||||
import type {
|
||||
Application,
|
||||
BalenaSDK,
|
||||
DeviceTypeJson,
|
||||
PineExpand,
|
||||
Release,
|
||||
} from 'balena-sdk';
|
||||
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;
|
||||
@ -105,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: `\
|
||||
@ -169,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();
|
||||
@ -200,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'];
|
||||
@ -244,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.',
|
||||
);
|
||||
@ -271,7 +244,7 @@ Can be repeated to add multiple certificates.\
|
||||
const preloader = new balenaPreload.Preloader(
|
||||
null,
|
||||
docker,
|
||||
appId,
|
||||
fleetSlug,
|
||||
commit,
|
||||
image,
|
||||
splashImage,
|
||||
@ -288,10 +261,17 @@ Can be repeated to add multiple certificates.\
|
||||
if (signal) {
|
||||
gotSignal = true;
|
||||
nodeCleanup.uninstall(); // don't call cleanup handler again
|
||||
preloader.cleanup().then(() => {
|
||||
// calling process.exit() won't inform parent process of signal
|
||||
process.kill(process.pid, signal);
|
||||
});
|
||||
preloader
|
||||
.cleanup()
|
||||
.then(() => {
|
||||
// calling process.exit() won't inform parent process of signal
|
||||
process.kill(process.pid, signal);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@ -308,7 +288,7 @@ Can be repeated to add multiple certificates.\
|
||||
preloader.on('error', reject);
|
||||
resolve(
|
||||
this.prepareAndPreload(preloader, balena, {
|
||||
appId,
|
||||
appId: fleetSlug,
|
||||
commit,
|
||||
pinDevice,
|
||||
}),
|
||||
@ -331,7 +311,6 @@ Can be repeated to add multiple certificates.\
|
||||
readonly applicationExpandOptions: PineExpand<Application> = {
|
||||
owns__release: {
|
||||
$select: ['id', 'commit', 'end_timestamp', 'composition'],
|
||||
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
|
||||
$expand: {
|
||||
contains__image: {
|
||||
$select: ['image'],
|
||||
@ -345,77 +324,75 @@ Can be repeated to add multiple certificates.\
|
||||
$filter: {
|
||||
status: 'success',
|
||||
},
|
||||
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
|
||||
},
|
||||
should_be_running__release: {
|
||||
$select: 'commit',
|
||||
},
|
||||
};
|
||||
|
||||
allDeviceTypes: DeviceTypeJson.DeviceType[];
|
||||
async getDeviceTypes() {
|
||||
if (this.allDeviceTypes === undefined) {
|
||||
const balena = getBalenaSdk();
|
||||
const deviceTypes = await balena.models.config.getDeviceTypes();
|
||||
this.allDeviceTypes = _.sortBy(deviceTypes, 'name');
|
||||
}
|
||||
return this.allDeviceTypes;
|
||||
}
|
||||
|
||||
isCurrentCommit(commit: string) {
|
||||
return commit === 'latest' || commit === 'current';
|
||||
}
|
||||
|
||||
async getDeviceTypesWithSameArch(deviceTypeSlug: string) {
|
||||
const deviceTypes = await this.getDeviceTypes();
|
||||
const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug });
|
||||
if (!deviceType) {
|
||||
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
|
||||
}
|
||||
return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value();
|
||||
}
|
||||
|
||||
async getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const deviceTypes = await this.getDeviceTypesWithSameArch(deviceTypeSlug);
|
||||
// TODO: remove the explicit types once https://github.com/balena-io/balena-sdk/pull/889 gets merged
|
||||
return balena.pine.get<
|
||||
Application,
|
||||
Array<
|
||||
ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
}
|
||||
>
|
||||
>({
|
||||
resource: 'my_application',
|
||||
options: {
|
||||
$filter: {
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: {
|
||||
dt: {
|
||||
slug: { $in: deviceTypes },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
owns__release: {
|
||||
$any: {
|
||||
$alias: 'r',
|
||||
$expr: {
|
||||
r: {
|
||||
status: 'success',
|
||||
try {
|
||||
await balena.models.deviceType.get(deviceTypeSlug);
|
||||
} catch {
|
||||
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
|
||||
}
|
||||
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
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: {
|
||||
dt: {
|
||||
is_of__cpu_architecture: {
|
||||
$any: {
|
||||
$alias: 'ioca',
|
||||
$expr: {
|
||||
ioca: {
|
||||
is_supported_by__device_type: {
|
||||
$any: {
|
||||
$alias: 'isbdt',
|
||||
$expr: {
|
||||
isbdt: {
|
||||
slug: deviceTypeSlug,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
$expand: this.applicationExpandOptions,
|
||||
$select: ['id', 'app_name', 'should_track_latest_release'],
|
||||
$orderby: 'app_name asc',
|
||||
owns__release: {
|
||||
$any: {
|
||||
$alias: 'r',
|
||||
$expr: {
|
||||
r: {
|
||||
status: 'success',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
$orderby: 'slug asc',
|
||||
})) as Array<
|
||||
ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
async selectApplication(deviceTypeSlug: string) {
|
||||
@ -439,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,
|
||||
})),
|
||||
});
|
||||
@ -514,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;
|
||||
nogitignore?: boolean;
|
||||
nolive: boolean;
|
||||
detached: boolean;
|
||||
service?: string[];
|
||||
system: boolean;
|
||||
env?: string[];
|
||||
'convert-eol'?: boolean;
|
||||
'noconvert-eol': boolean;
|
||||
'multi-dockerignore': boolean;
|
||||
'release-tag'?: string[];
|
||||
@ -218,16 +214,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,27 +223,6 @@ export default class PushCmd extends Command {
|
||||
'Have each service use its own .dockerignore file. See "balena help push".',
|
||||
char: 'm',
|
||||
default: false,
|
||||
exclusive: ['gitignore'],
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
nogitignore: flags.boolean({
|
||||
description:
|
||||
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
|
||||
char: 'G',
|
||||
hidden: true,
|
||||
default: false,
|
||||
}),
|
||||
}),
|
||||
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'],
|
||||
}),
|
||||
'release-tag': flags.string({
|
||||
description: stripIndent`
|
||||
@ -378,7 +343,6 @@ export default class PushCmd extends Command {
|
||||
source: options.source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
nogitignore: !options.gitignore,
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
@ -422,7 +386,6 @@ export default class PushCmd extends Command {
|
||||
multiDockerignore: options['multi-dockerignore'],
|
||||
nocache: options.nocache,
|
||||
pull: options.pull,
|
||||
nogitignore: !options.gitignore,
|
||||
noParentCheck: options['noparent-check'],
|
||||
nolive: options.nolive,
|
||||
detached: options.detached,
|
||||
|
86
lib/commands/release/finalize.ts
Normal file
86
lib/commands/release/finalize.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @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 { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
commitOrId: string | number;
|
||||
}
|
||||
|
||||
export default class ReleaseFinalizeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Finalize a release.
|
||||
|
||||
Finalize a release. Releases can be "draft" or "final", and this command
|
||||
changes a draft release into a final release. Draft releases can be created
|
||||
with the \`--draft\` option of the \`balena build\` or \`balena deploy\`
|
||||
commands.
|
||||
|
||||
Draft releases are not automatically deployed to devices tracking the latest
|
||||
release. For a draft release to be deployed to a device, the device should be
|
||||
explicity pinned to that release. Conversely, final releases may trigger immediate
|
||||
deployment to unpinned devices (subject to a device's polling period) and, for
|
||||
this reason, final releases cannot be changed back to draft status.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena release finalize a777f7345fe3d655c1c981aa642e5555',
|
||||
'$ balena release finalize 1234567',
|
||||
];
|
||||
|
||||
public static usage = 'release finalize <commitOrId>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'commitOrId',
|
||||
description: 'the commit or ID of the release to finalize',
|
||||
required: true,
|
||||
parse: (commitOrId: string) => tryAsInteger(commitOrId),
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleaseFinalizeCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const release = await balena.models.release.get(params.commitOrId, {
|
||||
$select: ['id', 'is_final'],
|
||||
});
|
||||
|
||||
if (release.is_final) {
|
||||
console.log(`Release ${params.commitOrId} is already finalized!`);
|
||||
return;
|
||||
}
|
||||
|
||||
await balena.models.release.finalize(release.id);
|
||||
console.log(`Release ${params.commitOrId} finalized`);
|
||||
}
|
||||
}
|
146
lib/commands/release/index.ts
Normal file
146
lib/commands/release/index.ts
Normal file
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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 Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import jsyaml = require('js-yaml');
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
composition?: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
commitOrId: string | number;
|
||||
}
|
||||
|
||||
export default class ReleaseCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Get info for a release.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena release a777f7345fe3d655c1c981aa642e5555',
|
||||
'$ balena release 1234567',
|
||||
];
|
||||
|
||||
public static usage = 'release <commitOrId>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
composition: flags.boolean({
|
||||
default: false,
|
||||
char: 'c',
|
||||
description: 'Return the release composition',
|
||||
exclusive: ['json', 'fields'],
|
||||
}),
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'commitOrId',
|
||||
description: 'the commit or ID of the release to get information',
|
||||
required: true,
|
||||
parse: (commitOrId: string) => tryAsInteger(commitOrId),
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ReleaseCmd,
|
||||
);
|
||||
|
||||
if (options.composition) {
|
||||
await this.showComposition(params.commitOrId);
|
||||
} else {
|
||||
await this.showReleaseInfo(params.commitOrId, options);
|
||||
}
|
||||
}
|
||||
|
||||
async showComposition(commitOrId: string | number) {
|
||||
const release = await getBalenaSdk().models.release.get(commitOrId, {
|
||||
$select: 'composition',
|
||||
});
|
||||
|
||||
if (isV14()) {
|
||||
this.outputMessage(jsyaml.dump(release.composition));
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(jsyaml.dump(release.composition));
|
||||
}
|
||||
}
|
||||
|
||||
async showReleaseInfo(commitOrId: string | number, options: FlagsDef) {
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
'commit',
|
||||
'created_at',
|
||||
'status',
|
||||
'semver',
|
||||
'is_final',
|
||||
'build_log',
|
||||
'start_timestamp',
|
||||
'end_timestamp',
|
||||
];
|
||||
|
||||
const release = await getBalenaSdk().models.release.get(commitOrId, {
|
||||
$select: fields,
|
||||
$expand: {
|
||||
release_tag: {
|
||||
$select: ['tag_key', 'value'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tagStr = release
|
||||
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
|
||||
.join('\n');
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(
|
||||
{
|
||||
tags: tagStr,
|
||||
...release,
|
||||
},
|
||||
fields,
|
||||
{
|
||||
displayNullValuesAs: 'N/a',
|
||||
...options,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Old output implementation
|
||||
const _ = await import('lodash');
|
||||
const values = _.mapValues(
|
||||
release,
|
||||
(val) => val ?? 'N/a',
|
||||
) as Dictionary<string>;
|
||||
values['tags'] = tagStr;
|
||||
|
||||
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
|
||||
}
|
||||
}
|
||||
}
|
101
lib/commands/releases.ts
Normal file
101
lib/commands/releases.ts
Normal file
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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 Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationNameNote } from '../utils/messages';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class ReleasesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all releases of a fleet.
|
||||
|
||||
List all releases of the given fleet.
|
||||
|
||||
${applicationNameNote.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = ['$ balena releases myorg/myfleet'];
|
||||
|
||||
public static usage = 'releases <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'fleet',
|
||||
description: 'fleet name or slug (preferred)',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ReleasesCmd,
|
||||
);
|
||||
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
'commit',
|
||||
'created_at',
|
||||
'status',
|
||||
'semver',
|
||||
'is_final',
|
||||
];
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const { getFleetSlug } = await import('../utils/sdk');
|
||||
|
||||
const releases = await balena.models.release.getAllByApplication(
|
||||
await getFleetSlug(balena, params.fleet),
|
||||
{ $select: fields },
|
||||
);
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(releases, fields, {
|
||||
displayNullValuesAs: 'N/a',
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -68,6 +68,7 @@ export default class ScanCmd extends Command {
|
||||
|
||||
public static primary = true;
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const _ = await import('lodash');
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import type { DataOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -35,15 +38,27 @@ export default class SettingsCmd extends Command {
|
||||
public static usage = 'settings';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(SettingsCmd);
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(SettingsCmd);
|
||||
|
||||
const settings = await getBalenaSdk().settings.getAll();
|
||||
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(settings));
|
||||
if (isV14()) {
|
||||
// Select all available fields for display
|
||||
const fields = Object.keys(settings);
|
||||
|
||||
await this.outputData(settings, fields, {
|
||||
noCapitalizeKeys: true,
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(settings));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +117,7 @@ export default class SshCmd extends Command {
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
@ -144,6 +145,7 @@ export default class SshCmd extends Command {
|
||||
const useProxy = !!proxyConfig && !options.noproxy;
|
||||
|
||||
// this will be a tunnelled SSH connection...
|
||||
await Command.checkNotUsingOfflineMode();
|
||||
await Command.checkLoggedIn();
|
||||
const deviceUuid = await getOnlineTargetDeviceUuid(
|
||||
sdk,
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -20,16 +20,13 @@ 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';
|
||||
import type { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
app?: string;
|
||||
application?: string;
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
@ -56,30 +53,19 @@ 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'],
|
||||
},
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -88,26 +74,21 @@ 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);
|
||||
}
|
||||
|
||||
const { tryAsInteger } = await import('../utils/validation');
|
||||
|
||||
let tags;
|
||||
let tags: ApplicationTag[] | DeviceTag[] | ReleaseTag[] = [];
|
||||
|
||||
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) {
|
||||
@ -127,11 +108,17 @@ export default class TagsCmd extends Command {
|
||||
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
|
||||
}
|
||||
|
||||
if (!tags || tags.length === 0) {
|
||||
if (tags.length === 0 && !options.json) {
|
||||
// TODO: Later change to output message
|
||||
throw new ExpectedError('No tags found');
|
||||
}
|
||||
|
||||
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
|
||||
if (isV14()) {
|
||||
await this.outputData(tags, ['tag_key', 'value'], options);
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
|
||||
}
|
||||
}
|
||||
|
||||
protected missingResourceMessage = stripIndent`
|
||||
|
@ -38,6 +38,8 @@ export default class UtilAvailableDrivesCmd extends Command {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(UtilAvailableDrivesCmd);
|
||||
|
||||
|
@ -57,6 +57,8 @@ export default class VersionCmd extends Command {
|
||||
|
||||
public static usage = 'version';
|
||||
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
all: flags.boolean({
|
||||
default: false,
|
||||
|
@ -38,7 +38,6 @@ export interface ReleaseTimestampsByVersion {
|
||||
* https://jel.ly.fish/ed8d2395-9323-418c-bb67-d11d32a17d00
|
||||
*/
|
||||
export class DeprecationChecker {
|
||||
protected static disabled = false; // for the benefit of testing
|
||||
readonly majorVersionFetchIntervalDays = 7;
|
||||
readonly expiryDays = 365;
|
||||
readonly deprecationDays = Math.ceil(this.expiryDays / 2);
|
||||
@ -105,7 +104,11 @@ export class DeprecationChecker {
|
||||
const url = this.getNpmUrl(version);
|
||||
let response: import('got').Response<Dictionary<any>> | undefined;
|
||||
try {
|
||||
response = await got(url, { responseType: 'json', retry: 0 });
|
||||
response = await got(url, {
|
||||
responseType: 'json',
|
||||
retry: 0,
|
||||
timeout: 4000,
|
||||
});
|
||||
} catch (e) {
|
||||
// 404 is expected if `version` hasn't been published yet
|
||||
if (e.response?.statusCode !== 404) {
|
||||
@ -131,7 +134,7 @@ or release date not available`);
|
||||
* `majorVersionFetchIntervalDays`.
|
||||
*/
|
||||
public async checkForNewReleasesIfNeeded() {
|
||||
if (DeprecationChecker.disabled) {
|
||||
if (process.env.BALENARC_UNSUPPORTED) {
|
||||
return; // for the benefit of code testing
|
||||
}
|
||||
await this.init();
|
||||
@ -182,7 +185,7 @@ or release date not available`);
|
||||
* in which case warn about it and conditionally throw an error.
|
||||
*/
|
||||
public async warnAndAbortIfDeprecated() {
|
||||
if (DeprecationChecker.disabled) {
|
||||
if (process.env.BALENARC_UNSUPPORTED) {
|
||||
return; // for the benefit of code testing
|
||||
}
|
||||
await this.init();
|
||||
@ -228,14 +231,4 @@ continue using this version of the CLI. However, note that the balenaCloud
|
||||
or openBalena backends may be updated in a way that is no longer compatible
|
||||
with this CLI version.`;
|
||||
}
|
||||
|
||||
/** Disable deprecation checks (for the benefit of code testing). */
|
||||
public static disable() {
|
||||
DeprecationChecker.disabled = true;
|
||||
}
|
||||
|
||||
/** Re-enable deprecation checks (for the benefit of code testing). */
|
||||
public static enable() {
|
||||
DeprecationChecker.disabled = false;
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ export class NotLoggedInError extends ExpectedError {}
|
||||
|
||||
export class InsufficientPrivilegesError extends ExpectedError {}
|
||||
|
||||
export class NotAvailableInOfflineModeError extends ExpectedError {}
|
||||
|
||||
export class InvalidPortMappingError extends ExpectedError {
|
||||
constructor(mapping: string) {
|
||||
super(`'${mapping}' is not a valid port mapping.`);
|
||||
@ -286,24 +288,3 @@ export const printErrorMessage = function (message: string) {
|
||||
export const printExpectedErrorMessage = function (message: string) {
|
||||
console.error(`${message}\n`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Print a friendly error message and exit the CLI with an error code, BYPASSING
|
||||
* error reporting through Sentry.io's platform (raven.Raven.captureException).
|
||||
* Note that lib/errors.ts provides top-level error handling code to catch any
|
||||
* otherwise uncaught errors, AND to report them through Sentry.io. But many
|
||||
* "expected" errors (say, a JSON parsing error in a file provided by the user)
|
||||
* don't warrant reporting through Sentry.io. For such mundane errors, catch
|
||||
* them and call this function.
|
||||
*
|
||||
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
|
||||
* If a specific process exit code x must be set, use process.exitCode = x
|
||||
*/
|
||||
export function exitWithExpectedError(message: string | Error): never {
|
||||
if (message instanceof Error) {
|
||||
({ message } = message);
|
||||
}
|
||||
|
||||
printErrorMessage(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
@ -14,19 +14,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as _ from 'lodash';
|
||||
import * as Mixpanel from 'mixpanel';
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { getBalenaSdk } from './utils/lazy';
|
||||
|
||||
const getMixpanel = _.once((balenaUrl: string) => {
|
||||
return Mixpanel.init('balena-main', {
|
||||
host: `api.${balenaUrl}`,
|
||||
path: '/mixpanel',
|
||||
protocol: 'https',
|
||||
});
|
||||
});
|
||||
import { getBalenaSdk, stripIndent } from './utils/lazy';
|
||||
|
||||
interface CachedUsername {
|
||||
token: string;
|
||||
@ -34,7 +24,7 @@ interface CachedUsername {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixpanel.com analytics tracking (information on balena CLI usage).
|
||||
* Track balena CLI usage events (product improvement analytics).
|
||||
*
|
||||
* @param commandSignature A string like, for example:
|
||||
* "push <fleetOrDevice>"
|
||||
@ -60,7 +50,6 @@ export async function trackCommand(commandSignature: string) {
|
||||
});
|
||||
}
|
||||
const settings = await import('balena-settings-client');
|
||||
const balenaUrl = settings.get<string>('balenaUrl');
|
||||
|
||||
const username = await (async () => {
|
||||
const getStorage = await import('balena-settings-storage');
|
||||
@ -94,8 +83,6 @@ export async function trackCommand(commandSignature: string) {
|
||||
}
|
||||
})();
|
||||
|
||||
const mixpanel = getMixpanel(balenaUrl);
|
||||
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry!.configureScope((scope) => {
|
||||
scope.setUser({
|
||||
@ -109,16 +96,53 @@ export async function trackCommand(commandSignature: string) {
|
||||
!process.env.BALENA_CLI_TEST_TYPE &&
|
||||
!process.env.BALENARC_NO_ANALYTICS
|
||||
) {
|
||||
await mixpanel.track(`[CLI] ${commandSignature}`, {
|
||||
distinct_id: username,
|
||||
version: packageJSON.version,
|
||||
node: process.version,
|
||||
arch: process.arch,
|
||||
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
|
||||
platform: process.platform,
|
||||
});
|
||||
const balenaUrl = settings.get<string>('balenaUrl');
|
||||
await sendEvent(balenaUrl, `[CLI] ${commandSignature}`, username);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the event tracking HTTPS request to balenaCloud's '/mixpanel' endpoint.
|
||||
*/
|
||||
async function sendEvent(balenaUrl: string, event: string, username?: string) {
|
||||
const { default: got } = await import('got');
|
||||
const trackData = {
|
||||
event,
|
||||
properties: {
|
||||
arch: process.arch,
|
||||
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
|
||||
distinct_id: username,
|
||||
mp_lib: 'node',
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
token: 'balena-main',
|
||||
version: packageJSON.version,
|
||||
},
|
||||
};
|
||||
const url = `https://api.${balenaUrl}/mixpanel/track`;
|
||||
const searchParams = {
|
||||
ip: 0,
|
||||
verbose: 0,
|
||||
data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
|
||||
};
|
||||
try {
|
||||
await got(url, { searchParams, retry: 0, timeout: 4000 });
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Event tracking error: ${e.message || e}`);
|
||||
}
|
||||
|
||||
if (e instanceof got.TimeoutError) {
|
||||
console.error(stripIndent`
|
||||
Timeout submitting analytics event to balenaCloud/openBalena.
|
||||
If you are using the balena CLI in an air-gapped environment with a filtered
|
||||
internet connection, set the BALENARC_OFFLINE_MODE=1 environment variable
|
||||
when using CLI commands that do not strictly require access to balenaCloud.
|
||||
`);
|
||||
}
|
||||
// Note: You can simulate a timeout using non-routable address 10.0.0.0
|
||||
}
|
||||
}
|
||||
|
@ -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 };
|
246
lib/framework/output.ts
Normal file
246
lib/framework/output.ts
Normal file
@ -0,0 +1,246 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getCliUx, getChalk } from '../utils/lazy';
|
||||
|
||||
/**
|
||||
* Used to extend FlagsDef for commands that output single-record data.
|
||||
* Exposed to user in command options.
|
||||
*/
|
||||
export interface DataOutputOptions {
|
||||
fields?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to extend FlagsDef for commands that output multi-record data.
|
||||
* Exposed to user in command options.
|
||||
*/
|
||||
export interface DataSetOutputOptions extends DataOutputOptions {
|
||||
filter?: string;
|
||||
'no-header'?: boolean;
|
||||
'no-truncate'?: boolean;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
// Not exposed to user
|
||||
export interface InternalOutputOptions {
|
||||
displayNullValuesAs?: string;
|
||||
hideNullOrUndefinedValues?: boolean;
|
||||
titleField?: string;
|
||||
noCapitalizeKeys?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) & InternalOutputOptions,
|
||||
) {
|
||||
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 & InternalOutputOptions,
|
||||
) {
|
||||
// Oclif expects fields to be specified in the format used in table headers (though lowercase)
|
||||
// By replacing underscores with spaces here, we can support both header format and actual field name
|
||||
// (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, ' ');
|
||||
|
||||
if (!options.json) {
|
||||
data = data.map((d) => {
|
||||
return processNullValues(d, options);
|
||||
});
|
||||
}
|
||||
|
||||
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 (similar to `resin-cli-visuals table.vertical`),
|
||||
* but supporting a subset of options from `cli-ux table` (--json and --fields)
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
async function outputDataItem(
|
||||
data: any,
|
||||
fields: string[],
|
||||
options: DataOutputOptions & InternalOutputOptions,
|
||||
) {
|
||||
let outData: typeof data = {};
|
||||
|
||||
// Convert comma separated list of fields in `options.fields` to array of correct format.
|
||||
// Note, user may have specified the true field name (e.g. `some_field`),
|
||||
// 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.displayNullValuesAs || options.hideNullOrUndefinedValues) &&
|
||||
!options.json
|
||||
) {
|
||||
outData = processNullValues(outData, options);
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
printLine(JSON.stringify(outData, undefined, 2));
|
||||
} else {
|
||||
// Find longest key, so we can align results
|
||||
const longestKeyLength = getLongestObjectKeyLength(outData);
|
||||
|
||||
if (options.titleField) {
|
||||
printTitle(data[options.titleField as keyof any[]], options);
|
||||
}
|
||||
|
||||
// Output one field per line
|
||||
for (let [k, v] of Object.entries(outData)) {
|
||||
const shim = ' '.repeat(longestKeyLength - k.length);
|
||||
let kDisplay = k.replace(/_/g, ' ');
|
||||
|
||||
// Start multiline values on the line below the field name
|
||||
if (typeof v === 'string' && v.includes('\n')) {
|
||||
v = `\n${v}`;
|
||||
}
|
||||
|
||||
if (!options.noCapitalizeKeys) {
|
||||
kDisplay = capitalize(kDisplay);
|
||||
}
|
||||
if (k !== options.titleField) {
|
||||
printLine(` ${bold(kDisplay) + shim} : ${v}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Amend null/undefined values in data as per options:
|
||||
* - options.displayNullValuesAs will replace the value with the specified string
|
||||
* - options.hideNullOrUndefinedValues will remove the property from the data
|
||||
*
|
||||
* @param data The data object to process
|
||||
* @param options Output options
|
||||
*
|
||||
* @returns a copy of the data with amended values.
|
||||
*/
|
||||
function processNullValues(data: any, options: InternalOutputOptions) {
|
||||
const dataCopy = { ...data };
|
||||
|
||||
Object.entries(dataCopy).forEach(([k, v]) => {
|
||||
if (v == null) {
|
||||
if (options.displayNullValuesAs) {
|
||||
dataCopy[k] = options.displayNullValuesAs;
|
||||
} else if (options.hideNullOrUndefinedValues) {
|
||||
delete dataCopy[k];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return dataCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a title with underscore
|
||||
*
|
||||
* @param title The title string to print
|
||||
* @param options Output options
|
||||
*/
|
||||
export function printTitle(
|
||||
title: string,
|
||||
options?: InternalOutputOptions & DataSetOutputOptions,
|
||||
) {
|
||||
if (!options?.['no-header']) {
|
||||
printLine(` ${capitalize(bold(title))}`);
|
||||
printLine(` ${bold('─'.repeat(title.length))}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printLine(s: any) {
|
||||
// Duplicating oclif cli-ux's default implementation here,
|
||||
// but using this one explicitly for ease of testing
|
||||
process.stdout.write(s + '\n');
|
||||
}
|
||||
|
||||
function capitalize(s: string) {
|
||||
return `${s[0].toUpperCase()}${s.slice(1)}`;
|
||||
}
|
||||
|
||||
function bold(s: string) {
|
||||
return getChalk().bold(s);
|
||||
}
|
||||
|
||||
function getLongestObjectKeyLength(o: any): number {
|
||||
return Math.max(0, ...Object.keys(o).map((k) => k.length));
|
||||
}
|
@ -28,7 +28,7 @@ export const trackPromise = new Promise((resolve) => {
|
||||
* parsed by oclif, but before the command's run() function is called.
|
||||
* See: https://oclif.io/docs/hooks
|
||||
*
|
||||
* This hook is used to track CLI command signatures with mixpanel.
|
||||
* This hook is used to track CLI command signatures (usage analytics).
|
||||
* A command signature is something like "env add NAME [VALUE]". That's
|
||||
* literally so: 'NAME' and 'VALUE' are NOT replaced with actual values.
|
||||
*/
|
||||
|
@ -53,6 +53,12 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
||||
if (extractBooleanFlag(cmdSlice, '--debug')) {
|
||||
process.env.DEBUG = '1';
|
||||
}
|
||||
// support global --v-next flag
|
||||
if (extractBooleanFlag(cmdSlice, '--v-next')) {
|
||||
const { version } = await import('../package.json');
|
||||
const { inc } = await import('semver');
|
||||
process.env.BALENA_CLI_VERSION_OVERRIDE = inc(version, 'major') || '';
|
||||
}
|
||||
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
|
||||
}
|
||||
|
||||
@ -131,6 +137,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) {
|
||||
|
@ -58,7 +58,13 @@ export function normalizeEnvVar(varName: string) {
|
||||
process.env[varName] = parseBoolEnvVar(varName) ? '1' : '';
|
||||
}
|
||||
|
||||
const bootstrapVars = ['DEBUG', 'BALENARC_NO_SENTRY'];
|
||||
const bootstrapVars = [
|
||||
'BALENARC_NO_SENTRY',
|
||||
'BALENARC_NO_ANALYTICS',
|
||||
'BALENARC_OFFLINE_MODE',
|
||||
'BALENARC_UNSUPPORTED',
|
||||
'DEBUG',
|
||||
];
|
||||
|
||||
export function normalizeEnvVars(varNames: string[] = bootstrapVars) {
|
||||
for (const varName of varNames) {
|
||||
@ -66,6 +72,17 @@ export function normalizeEnvVars(varNames: string[] = bootstrapVars) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the individual env vars implied by BALENARC_OFFLINE_MODE.
|
||||
*/
|
||||
export function setOfflineModeEnvVars() {
|
||||
if (process.env.BALENARC_OFFLINE_MODE) {
|
||||
process.env.BALENARC_UNSUPPORTED = '1';
|
||||
process.env.BALENARC_NO_SENTRY = '1';
|
||||
process.env.BALENARC_NO_ANALYTICS = '1';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the 'pkgExec' command, used as a way to provide a Node.js
|
||||
* interpreter for child_process.spawn()-like operations when the CLI is
|
||||
|
@ -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:
|
||||
@ -130,22 +140,35 @@ export async function downloadOSImage(
|
||||
console.info(`Getting device operating system for ${deviceType}`);
|
||||
|
||||
if (!OSVersion) {
|
||||
console.warn('OS version not specified: using latest stable version');
|
||||
console.warn('OS version not specified: using latest released version');
|
||||
}
|
||||
|
||||
OSVersion = OSVersion
|
||||
? await resolveOSVersion(deviceType, OSVersion)
|
||||
: 'default';
|
||||
|
||||
const displayVersion = OSVersion === 'default' ? '' : ` ${OSVersion}`;
|
||||
// Override the default zlib flush value as we've seen cases of
|
||||
// incomplete files being identified as successful downloads when using Z_SYNC_FLUSH.
|
||||
// Using Z_NO_FLUSH results in a Z_BUF_ERROR instead of a corrupt image file.
|
||||
// https://github.com/nodejs/node/blob/master/doc/api/zlib.md#zlib-constants
|
||||
// Hopefully this is a temporary workaround until we can resolve
|
||||
// some ongoing issues with the os download stream.
|
||||
process.env.ZLIB_FLUSH = 'Z_NO_FLUSH';
|
||||
|
||||
const manager = await import('balena-image-manager');
|
||||
const stream = await manager.get(deviceType, OSVersion);
|
||||
|
||||
const displayVersion = await new Promise((resolve, reject) => {
|
||||
stream.on('error', reject);
|
||||
stream.on('balena-image-manager:resolved-version', resolve);
|
||||
});
|
||||
|
||||
const visuals = getVisuals();
|
||||
const bar = new visuals.Progress(`Downloading Device OS${displayVersion}`);
|
||||
const bar = new visuals.Progress(
|
||||
`Downloading balenaOS version ${displayVersion}`,
|
||||
);
|
||||
const spinner = new visuals.Spinner(
|
||||
`Downloading Device OS${displayVersion} (size unknown)`,
|
||||
`Downloading balenaOS version ${displayVersion} (size unknown)`,
|
||||
);
|
||||
|
||||
stream.on('progress', (state: any) => {
|
||||
@ -175,31 +198,83 @@ export async function downloadOSImage(
|
||||
const streamToPromise = await import('stream-to-promise');
|
||||
await streamToPromise(stream.pipe(output));
|
||||
|
||||
console.info('The image was downloaded successfully');
|
||||
console.info(
|
||||
`balenaOS image version ${displayVersion} downloaded successfully`,
|
||||
);
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
async function resolveOSVersion(deviceType: string, version: string) {
|
||||
if (version !== 'menu') {
|
||||
async function resolveOSVersion(
|
||||
deviceType: string,
|
||||
version: string,
|
||||
): Promise<string> {
|
||||
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 { versions: vs, recommended } =
|
||||
await getBalenaSdk().models.os.getSupportedVersions(deviceType);
|
||||
async function selectOSVersionFromMenu(
|
||||
deviceType: string,
|
||||
esr: boolean,
|
||||
): Promise<string> {
|
||||
const vs = await getOsVersions(deviceType, esr);
|
||||
|
||||
const choices = vs.map((v) => ({
|
||||
value: v,
|
||||
name: `v${v}` + (v === recommended ? ' (recommended)' : ''),
|
||||
value: v.raw_version,
|
||||
name: formatOsVersion(v),
|
||||
}));
|
||||
|
||||
return getCliForm().ask({
|
||||
message: 'Select the OS version:',
|
||||
type: 'list',
|
||||
choices,
|
||||
default: recommended,
|
||||
default: vs[0]?.raw_version,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the output of sdk.models.os.getAvailableOsVersions(), resolving
|
||||
* device type aliases and filtering with regard to ESR versions.
|
||||
*/
|
||||
export async function getOsVersions(
|
||||
deviceType: string,
|
||||
esr: boolean,
|
||||
): Promise<SDK.OsVersion[]> {
|
||||
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) {
|
||||
// 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,8 +97,55 @@ export const deviceType = flags.string({
|
||||
required: true,
|
||||
});
|
||||
|
||||
export const deviceTypeIgnored = {
|
||||
...(isV14()
|
||||
? {}
|
||||
: {
|
||||
type: flags.string({
|
||||
description: 'ignored - no longer required',
|
||||
char: 't',
|
||||
required: false,
|
||||
hidden: true,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export const json: IBooleanFlag<boolean> = flags.boolean({
|
||||
char: 'j',
|
||||
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)`,
|
||||
}),
|
||||
};
|
||||
|
14
lib/utils/compose-types.d.ts
vendored
14
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;
|
||||
noParentCheck: boolean;
|
||||
projectName: string;
|
||||
projectName?: string;
|
||||
projectPath: string;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
@ -63,12 +63,9 @@ export interface ComposeCliFlags {
|
||||
dockerfile?: string;
|
||||
logs: boolean;
|
||||
nologs: boolean;
|
||||
gitignore: boolean;
|
||||
'multi-dockerignore': boolean;
|
||||
nogitignore: boolean;
|
||||
'noparent-check': boolean;
|
||||
'registry-secrets'?: RegistrySecrets;
|
||||
'convert-eol': boolean;
|
||||
'noconvert-eol': boolean;
|
||||
projectName?: string;
|
||||
}
|
||||
@ -83,7 +80,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 +92,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;
|
||||
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
|
||||
}
|
||||
|
@ -15,21 +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 Logger = require('./logger');
|
||||
import { ProgressCallback } from 'docker-progress';
|
||||
|
||||
/**
|
||||
* @returns Promise<{import('./compose-types').ComposeOpts}>
|
||||
*/
|
||||
export function generateOpts(options) {
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
if (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,
|
||||
@ -37,31 +48,29 @@ export function generateOpts(options) {
|
||||
convertEol: !options['noconvert-eol'],
|
||||
dockerfilePath: options.dockerfile,
|
||||
multiDockerignore: !!options['multi-dockerignore'],
|
||||
nogitignore: !options.gitignore,
|
||||
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 | null} projectName
|
||||
* @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, projectName = null) {
|
||||
const yml = require('js-yaml');
|
||||
const compose = require('resin-compose-parse');
|
||||
export function createProject(
|
||||
composePath: string,
|
||||
composeStr: string,
|
||||
projectName = '',
|
||||
imageTag = '',
|
||||
): 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, {
|
||||
schema: yml.FAILSAFE_SCHEMA,
|
||||
});
|
||||
const rawComposition = yml.load(composeStr);
|
||||
const composition = compose.normalize(rawComposition);
|
||||
|
||||
projectName ??= path.basename(composePath);
|
||||
projectName ||= path.basename(composePath);
|
||||
|
||||
const descriptors = compose.parse(composition).map(function (descr) {
|
||||
// generate an image name based on the project and service names
|
||||
@ -71,9 +80,9 @@ export function createProject(composePath, composeStr, projectName = null) {
|
||||
descr.image.context != null &&
|
||||
descr.image.tag == null
|
||||
) {
|
||||
descr.image.tag = [projectName, descr.serviceName]
|
||||
.join('_')
|
||||
.toLowerCase();
|
||||
const { makeImageName } =
|
||||
require('./compose_ts') as typeof import('./compose_ts');
|
||||
descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag);
|
||||
}
|
||||
return descr;
|
||||
});
|
||||
@ -85,114 +94,20 @@ export function createProject(composePath, composeStr, projectName = null) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>}
|
||||
*/
|
||||
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 });
|
||||
|
||||
@ -222,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];
|
||||
@ -266,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',
|
||||
@ -294,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 || '';
|
||||
@ -315,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];
|
||||
}
|
||||
@ -348,89 +259,22 @@ export const authorizePush = function (
|
||||
.catch(() => '');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('dockerode')} docker
|
||||
* @param {string} token
|
||||
* @param {Array<import('./compose-types').TaggedImage>} images
|
||||
* @param {(serviceImage: import('balena-release/build/models').ImageModel, props: object) => void} afterEach
|
||||
*/
|
||||
export const pushAndUpdateServiceImages = function (
|
||||
docker,
|
||||
token,
|
||||
images,
|
||||
afterEach,
|
||||
) {
|
||||
const { DockerProgress } = require('docker-progress');
|
||||
const { retry } = require('./helpers');
|
||||
const tty = require('./tty')(process.stdout);
|
||||
const Bluebird = require('bluebird');
|
||||
|
||||
const opts = { authconfig: { registrytoken: token } };
|
||||
|
||||
const progress = new DockerProgress({ docker });
|
||||
const renderer = pushProgressRenderer(
|
||||
tty,
|
||||
getChalk().blue('[Push]') + ' ',
|
||||
);
|
||||
const reporters = progress.aggregateProgress(images.length, renderer);
|
||||
|
||||
return Bluebird.using(tty.cursorHidden(), () =>
|
||||
Promise.all(
|
||||
images.map(({ serviceImage, localImage, props, logs }, index) =>
|
||||
Promise.all([
|
||||
localImage.inspect().then((img) => img.Size),
|
||||
retry({
|
||||
// @ts-ignore
|
||||
func: () => progress.push(localImage.name, reporters[index], opts),
|
||||
maxAttempts: 3, // try calling func 3 times (max)
|
||||
// @ts-ignore
|
||||
label: localImage.name, // label for retry log messages
|
||||
initialDelayMs: 2000, // wait 2 seconds before the 1st retry
|
||||
backoffScaler: 1.4, // wait multiplier for each retry
|
||||
}).finally(renderer.end),
|
||||
])
|
||||
.then(
|
||||
/** @type {([number, string]) => void} */
|
||||
function ([size, digest]) {
|
||||
serviceImage.image_size = size;
|
||||
serviceImage.content_hash = digest;
|
||||
serviceImage.build_log = logs;
|
||||
serviceImage.dockerfile = props.dockerfile;
|
||||
serviceImage.project_type = props.projectType;
|
||||
if (props.startTime) {
|
||||
serviceImage.start_timestamp = props.startTime;
|
||||
}
|
||||
if (props.endTime) {
|
||||
serviceImage.end_timestamp = props.endTime;
|
||||
}
|
||||
serviceImage.push_timestamp = new Date();
|
||||
serviceImage.status = 'success';
|
||||
},
|
||||
)
|
||||
.catch(function (e) {
|
||||
serviceImage.error_message = '' + e;
|
||||
serviceImage.status = 'failed';
|
||||
throw e;
|
||||
})
|
||||
.finally(() => afterEach?.(serviceImage, props)),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// 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)}%`;
|
||||
};
|
||||
|
||||
var 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);
|
||||
@ -444,15 +288,39 @@ var 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._handleInterrupt = this._handleInterrupt.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');
|
||||
@ -470,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
|
||||
@ -480,49 +347,43 @@ 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;
|
||||
}
|
||||
|
||||
_handleInterrupt() {
|
||||
this._cancelled = true;
|
||||
this.end();
|
||||
return process.exit(130); // 128 + SIGINT
|
||||
}
|
||||
|
||||
start() {
|
||||
process.on('SIGINT', this._handleInterrupt);
|
||||
this._tty.hideCursor();
|
||||
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;
|
||||
process.removeListener('SIGINT', this._handleInterrupt);
|
||||
this._runloop?.end();
|
||||
this._runloop = null;
|
||||
this._runloop = undefined;
|
||||
|
||||
this._clear();
|
||||
this._renderStatus(true);
|
||||
@ -543,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;
|
||||
@ -570,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);
|
||||
@ -599,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);
|
||||
@ -619,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;
|
||||
@ -642,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;
|
||||
@ -659,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;
|
||||
@ -689,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;
|
@ -18,8 +18,9 @@ import { flags } from '@oclif/command';
|
||||
import { BalenaSDK } from 'balena-sdk';
|
||||
import type { TransposeOptions } from 'docker-qemu-transpose';
|
||||
import type * as Dockerode from 'dockerode';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import jsyaml = require('js-yaml');
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
BuildConfig,
|
||||
@ -43,7 +44,6 @@ import type { DeviceInfo } from './device/api';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
import { exists } from './which';
|
||||
import jsyaml = require('js-yaml');
|
||||
|
||||
const allowedContractTypes = ['sw.application', 'sw.block'];
|
||||
|
||||
@ -104,8 +104,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
|
||||
@ -117,6 +115,7 @@ export async function loadProject(
|
||||
logger: Logger,
|
||||
opts: ComposeOpts,
|
||||
image?: string,
|
||||
imageTag?: string,
|
||||
): Promise<ComposeProject> {
|
||||
const compose = await import('resin-compose-parse');
|
||||
const { createProject } = await import('./compose');
|
||||
@ -155,7 +154,12 @@ export async function loadProject(
|
||||
}
|
||||
}
|
||||
logger.logDebug('Creating project...');
|
||||
return createProject(opts.projectPath, composeStr, opts.projectName);
|
||||
return createProject(
|
||||
opts.projectPath,
|
||||
composeStr,
|
||||
opts.projectName,
|
||||
imageTag,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -231,13 +235,13 @@ interface BuildTaskPlus extends MultiBuild.BuildTask {
|
||||
logBuffer?: string[];
|
||||
}
|
||||
|
||||
interface Renderer {
|
||||
export interface Renderer {
|
||||
start: () => void;
|
||||
end: (buildSummaryByService?: Dictionary<string>) => void;
|
||||
streams: Dictionary<NodeJS.ReadWriteStream>;
|
||||
}
|
||||
|
||||
export async function buildProject(opts: {
|
||||
export interface BuildProjectOpts {
|
||||
docker: Dockerode;
|
||||
logger: Logger;
|
||||
projectPath: string;
|
||||
@ -250,84 +254,100 @@ export async function buildProject(opts: {
|
||||
inlineLogs?: boolean;
|
||||
convertEol: boolean;
|
||||
dockerfilePath?: string;
|
||||
nogitignore: boolean;
|
||||
multiDockerignore: boolean;
|
||||
}): Promise<BuiltImage[]> {
|
||||
const { logger, projectName } = opts;
|
||||
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
|
||||
}
|
||||
|
||||
let buildSummaryByService: Dictionary<string> | undefined;
|
||||
export async function buildProject(
|
||||
opts: BuildProjectOpts,
|
||||
): Promise<BuiltImage[]> {
|
||||
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
|
||||
const compose = await import('resin-compose-parse');
|
||||
const imageDescriptors = compose.parse(opts.composition);
|
||||
const imageDescriptorsByServiceName = _.keyBy(
|
||||
imageDescriptors,
|
||||
'serviceName',
|
||||
);
|
||||
const renderer = await startRenderer({ imageDescriptors, ...opts });
|
||||
let buildSummaryByService: Dictionary<string> | undefined;
|
||||
try {
|
||||
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
|
||||
|
||||
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
|
||||
|
||||
const tarStream = await tarDirectory(opts.projectPath, opts);
|
||||
|
||||
const tasks: BuildTaskPlus[] = await makeBuildTasks(
|
||||
opts.composition,
|
||||
tarStream,
|
||||
const { awaitInterruptibleTask } = await import('./helpers');
|
||||
const [images, summaryMsgByService] = await awaitInterruptibleTask(
|
||||
$buildProject,
|
||||
imageDescriptors,
|
||||
renderer,
|
||||
opts,
|
||||
logger,
|
||||
projectName,
|
||||
);
|
||||
|
||||
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
|
||||
|
||||
const transposeOptArray: Array<TransposeOptions | undefined> =
|
||||
await Promise.all(
|
||||
tasks.map((task) => {
|
||||
// Setup emulation if needed
|
||||
if (needsQemu && !task.external) {
|
||||
return qemuTransposeBuildStream({ task, ...opts });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
// transposeOptions may be undefined. That's OK.
|
||||
transposeOptArray.map((transposeOptions, index) =>
|
||||
setTaskProgressHooks({
|
||||
task: tasks[index],
|
||||
renderer,
|
||||
transposeOptions,
|
||||
...opts,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
logger.logDebug('Prepared tasks; building...');
|
||||
|
||||
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
|
||||
const builder = await import('resin-multibuild');
|
||||
|
||||
const builtImages = await builder.performBuilds(
|
||||
tasks,
|
||||
opts.docker,
|
||||
BALENA_ENGINE_TMP_PATH,
|
||||
);
|
||||
|
||||
const [images, summaryMsgByService] = await inspectBuiltImages({
|
||||
builtImages,
|
||||
imageDescriptorsByServiceName,
|
||||
tasks,
|
||||
...opts,
|
||||
});
|
||||
buildSummaryByService = summaryMsgByService;
|
||||
|
||||
return images;
|
||||
} finally {
|
||||
renderer.end(buildSummaryByService);
|
||||
}
|
||||
}
|
||||
|
||||
async function $buildProject(
|
||||
imageDescriptors: ImageDescriptor[],
|
||||
renderer: Renderer,
|
||||
opts: BuildProjectOpts,
|
||||
): Promise<[BuiltImage[], Dictionary<string>]> {
|
||||
const { logger, projectName } = opts;
|
||||
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
|
||||
|
||||
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
|
||||
|
||||
const tarStream = await tarDirectory(opts.projectPath, opts);
|
||||
|
||||
const tasks: BuildTaskPlus[] = await makeBuildTasks(
|
||||
opts.composition,
|
||||
tarStream,
|
||||
opts,
|
||||
logger,
|
||||
projectName,
|
||||
);
|
||||
|
||||
const imageDescriptorsByServiceName = _.keyBy(
|
||||
imageDescriptors,
|
||||
'serviceName',
|
||||
);
|
||||
|
||||
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
|
||||
|
||||
const transposeOptArray: Array<TransposeOptions | undefined> =
|
||||
await Promise.all(
|
||||
tasks.map((task) => {
|
||||
// Setup emulation if needed
|
||||
if (needsQemu && !task.external) {
|
||||
return qemuTransposeBuildStream({ task, ...opts });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
// transposeOptions may be undefined. That's OK.
|
||||
transposeOptArray.map((transposeOptions, index) =>
|
||||
setTaskProgressHooks({
|
||||
task: tasks[index],
|
||||
renderer,
|
||||
transposeOptions,
|
||||
...opts,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
logger.logDebug('Prepared tasks; building...');
|
||||
|
||||
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
|
||||
const builder = await import('resin-multibuild');
|
||||
|
||||
const builtImages = await builder.performBuilds(
|
||||
tasks,
|
||||
opts.docker,
|
||||
BALENA_ENGINE_TMP_PATH,
|
||||
);
|
||||
|
||||
return await inspectBuiltImages({
|
||||
builtImages,
|
||||
imageDescriptorsByServiceName,
|
||||
tasks,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
async function startRenderer({
|
||||
imageDescriptors,
|
||||
inlineLogs,
|
||||
@ -393,6 +413,18 @@ async function installQemuIfNeeded({
|
||||
return needsQemu;
|
||||
}
|
||||
|
||||
export function makeImageName(
|
||||
projectName: string,
|
||||
serviceName: string,
|
||||
tag?: string,
|
||||
) {
|
||||
let name = `${projectName}_${serviceName}`;
|
||||
if (tag) {
|
||||
name = [name, tag].map((s) => s.replace(/:/g, '_')).join(':');
|
||||
}
|
||||
return name.toLowerCase();
|
||||
}
|
||||
|
||||
function setTaskAttributes({
|
||||
tasks,
|
||||
buildOpts,
|
||||
@ -408,7 +440,7 @@ function setTaskAttributes({
|
||||
const d = imageDescriptorsByServiceName[task.serviceName];
|
||||
// multibuild (splitBuildStream) parses the composition internally so
|
||||
// any tags we've set before are lost; re-assign them here
|
||||
task.tag ??= [projectName, task.serviceName].join('_').toLowerCase();
|
||||
task.tag ??= makeImageName(projectName, task.serviceName, buildOpts.t);
|
||||
if (isBuildConfig(d.image)) {
|
||||
d.image.tag = task.tag;
|
||||
}
|
||||
@ -689,7 +721,7 @@ export async function getServiceDirsFromComposition(
|
||||
* Return true if `image` is actually a docker-compose.yml `services.service.build`
|
||||
* configuration object, rather than an "external image" (`services.service.image`).
|
||||
*
|
||||
* The `image` argument may therefore refere to either a `build` or `image` property
|
||||
* The `image` argument may therefore refer to either a `build` or `image` property
|
||||
* of a service in a docker-compose.yml file, which is a bit confusing but it matches
|
||||
* the `ImageDescriptor.image` property as defined by `resin-compose-parse`.
|
||||
*
|
||||
@ -712,41 +744,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;
|
||||
if (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,
|
||||
preFinalizeCallback,
|
||||
}: TarDirectoryOptions,
|
||||
): Promise<import('stream').Readable> {
|
||||
require('assert').strict.equal(nogitignore, true);
|
||||
const { filterFilesWithDockerignore } = await import('./ignore');
|
||||
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
|
||||
|
||||
@ -862,44 +872,8 @@ function printDockerignoreWarn(
|
||||
}
|
||||
}
|
||||
if (msg.length) {
|
||||
logFunc.call(logger, [' ', hr, ...msg, hr].join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function printGitignoreWarn(
|
||||
dockerignoreFile: string,
|
||||
gitignoreFiles: string[],
|
||||
) {
|
||||
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'));
|
||||
const { warnify } = require('./messages') as typeof import('./messages');
|
||||
logFunc.call(logger, ' \n' + warnify(msg.join('\n'), ''));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1007,9 +981,7 @@ export async function makeBuildTasks(
|
||||
infoStr = `build [${task.context}]`;
|
||||
}
|
||||
logger.logDebug(` ${task.serviceName}: ${infoStr}`);
|
||||
// Workaround for Docker v20.10 + single-arch base images. See:
|
||||
// https://www.flowdock.com/app/rulemotion/i-cli/threads/RuSu1KiWOn62xaGy7O2sn8m8BUc
|
||||
task.dockerPlatform = 'none';
|
||||
task.logger = logger.getAdapter();
|
||||
});
|
||||
|
||||
logger.logDebug(
|
||||
@ -1043,53 +1015,52 @@ async function performResolution(
|
||||
releaseHash: string,
|
||||
preprocessHook?: (dockerfile: string) => string,
|
||||
): Promise<MultiBuild.BuildTask[]> {
|
||||
const { cloneTarStream } = await import('tar-utils');
|
||||
const multiBuild = await import('resin-multibuild');
|
||||
|
||||
return await new Promise<MultiBuild.BuildTask[]>((resolve, reject) => {
|
||||
const buildTasks = multiBuild.performResolution(
|
||||
tasks,
|
||||
deviceInfo.arch,
|
||||
deviceInfo.deviceType,
|
||||
{ error: [reject] },
|
||||
{
|
||||
BALENA_RELEASE_HASH: releaseHash,
|
||||
BALENA_APP_NAME: appName,
|
||||
},
|
||||
preprocessHook,
|
||||
);
|
||||
(async () => {
|
||||
try {
|
||||
// Do one task at a time in order to reduce peak memory usage. Resolves to buildTasks.
|
||||
for (const buildTask of buildTasks) {
|
||||
// buildStream is falsy for "external" tasks (image pull)
|
||||
if (!buildTask.buildStream) {
|
||||
continue;
|
||||
}
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
// Consume each task.buildStream in order to trigger the
|
||||
// resolution events that define fields like:
|
||||
// task.dockerfile, task.dockerfilePath,
|
||||
// task.projectType, task.resolved
|
||||
// This mimics what is currently done in `resin-builder`.
|
||||
buildTask.buildStream = await cloneTarStream(buildTask.buildStream);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
if (error || (!buildTask.external && !buildTask.resolved)) {
|
||||
const cause = error ? `${error}\n` : '';
|
||||
throw new ExpectedError(
|
||||
`${cause}Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
resolve(buildTasks);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
})();
|
||||
const resolveListeners: MultiBuild.ResolveListeners = {};
|
||||
const resolvePromise = new Promise<never>((_resolve, reject) => {
|
||||
resolveListeners.error = [reject];
|
||||
});
|
||||
const buildTasks = multiBuild.performResolution(
|
||||
tasks,
|
||||
deviceInfo.arch,
|
||||
deviceInfo.deviceType,
|
||||
resolveListeners,
|
||||
{
|
||||
BALENA_RELEASE_HASH: releaseHash,
|
||||
BALENA_APP_NAME: appName,
|
||||
},
|
||||
preprocessHook,
|
||||
);
|
||||
await Promise.race([resolvePromise, resolveTasks(buildTasks)]);
|
||||
return buildTasks;
|
||||
}
|
||||
|
||||
async function resolveTasks(buildTasks: MultiBuild.BuildTask[]) {
|
||||
const { cloneTarStream } = await import('tar-utils');
|
||||
// Do one task at a time in order to reduce peak memory usage. Resolves to buildTasks.
|
||||
for (const buildTask of buildTasks) {
|
||||
// buildStream is falsy for "external" tasks (image pull)
|
||||
if (!buildTask.buildStream) {
|
||||
continue;
|
||||
}
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
// Consume each task.buildStream in order to trigger the
|
||||
// resolution events that define fields like:
|
||||
// task.dockerfile, task.dockerfilePath,
|
||||
// task.projectType, task.resolved
|
||||
// This mimics what is currently done in `resin-builder`.
|
||||
buildTask.buildStream = await cloneTarStream(buildTask.buildStream);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
if (error || (!buildTask.external && !buildTask.resolved)) {
|
||||
const cause = error ? `${error}\n` : '';
|
||||
throw new ExpectedError(
|
||||
`${cause}Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1264,15 +1235,101 @@ async function getTokenForPreviousRepos(
|
||||
return token;
|
||||
}
|
||||
|
||||
async function pushAndUpdateServiceImages(
|
||||
docker: Dockerode,
|
||||
token: string,
|
||||
images: TaggedImage[],
|
||||
afterEach: (
|
||||
serviceImage: import('balena-release/build/models').ImageModel,
|
||||
props: object,
|
||||
) => void,
|
||||
) {
|
||||
const { DockerProgress } = await import('docker-progress');
|
||||
const { retry } = await import('./helpers');
|
||||
const { pushProgressRenderer } = await import('./compose');
|
||||
const tty = (await import('./tty'))(process.stdout);
|
||||
const opts = { authconfig: { registrytoken: token } };
|
||||
const progress = new DockerProgress({ docker });
|
||||
const renderer = pushProgressRenderer(
|
||||
tty,
|
||||
getChalk().blue('[Push]') + ' ',
|
||||
);
|
||||
const reporters = progress.aggregateProgress(images.length, renderer);
|
||||
|
||||
const pushImage = async (
|
||||
localImage: Dockerode.Image,
|
||||
index: number,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
// TODO 'localImage as any': find out exactly why tsc warns about
|
||||
// 'name' that exists as a matter of fact, with a value similar to:
|
||||
// "name": "registry2.balena-cloud.com/v2/aa27790dff571ec7d2b4fbcf3d4648d5:latest"
|
||||
const imgName: string = (localImage as any).name || '';
|
||||
const imageDigest: string = await retry({
|
||||
func: () => progress.push(imgName, reporters[index], opts),
|
||||
maxAttempts: 3, // try calling func 3 times (max)
|
||||
label: imgName, // label for retry log messages
|
||||
initialDelayMs: 2000, // wait 2 seconds before the 1st retry
|
||||
backoffScaler: 1.4, // wait multiplier for each retry
|
||||
});
|
||||
if (!imageDigest) {
|
||||
throw new ExpectedError(stripIndent`\
|
||||
Unable to extract image digest (content hash) from image upload progress stream for image:
|
||||
${imgName}`);
|
||||
}
|
||||
return imageDigest;
|
||||
} finally {
|
||||
renderer.end();
|
||||
}
|
||||
};
|
||||
|
||||
const inspectAndPushImage = async (
|
||||
{ serviceImage, localImage, props, logs }: TaggedImage,
|
||||
index: number,
|
||||
) => {
|
||||
try {
|
||||
const [imgInfo, imgDigest] = await Promise.all([
|
||||
localImage.inspect(),
|
||||
pushImage(localImage, index),
|
||||
]);
|
||||
serviceImage.image_size = imgInfo.Size;
|
||||
serviceImage.content_hash = imgDigest;
|
||||
serviceImage.build_log = logs;
|
||||
serviceImage.dockerfile = props.dockerfile;
|
||||
serviceImage.project_type = props.projectType;
|
||||
if (props.startTime) {
|
||||
serviceImage.start_timestamp = props.startTime;
|
||||
}
|
||||
if (props.endTime) {
|
||||
serviceImage.end_timestamp = props.endTime;
|
||||
}
|
||||
serviceImage.push_timestamp = new Date();
|
||||
serviceImage.status = 'success';
|
||||
} catch (error) {
|
||||
serviceImage.error_message = '' + error;
|
||||
serviceImage.status = 'failed';
|
||||
throw error;
|
||||
} finally {
|
||||
await afterEach(serviceImage, props);
|
||||
}
|
||||
};
|
||||
|
||||
tty.hideCursor();
|
||||
try {
|
||||
await Promise.all(images.map(inspectAndPushImage));
|
||||
} finally {
|
||||
tty.showCursor();
|
||||
}
|
||||
}
|
||||
|
||||
async function pushServiceImages(
|
||||
docker: import('dockerode'),
|
||||
docker: Dockerode,
|
||||
logger: Logger,
|
||||
pineClient: ReturnType<typeof import('balena-release').createClient>,
|
||||
taggedImages: TaggedImage[],
|
||||
token: string,
|
||||
skipLogUpload: boolean,
|
||||
): Promise<void> {
|
||||
const { pushAndUpdateServiceImages } = await import('./compose');
|
||||
const releaseMod = await import('balena-release');
|
||||
logger.logInfo('Pushing images to registry...');
|
||||
await pushAndUpdateServiceImages(
|
||||
@ -1295,7 +1352,7 @@ async function pushServiceImages(
|
||||
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
|
||||
|
||||
export async function deployProject(
|
||||
docker: import('dockerode'),
|
||||
docker: Dockerode,
|
||||
logger: Logger,
|
||||
composition: Composition,
|
||||
images: BuiltImage[],
|
||||
@ -1344,20 +1401,25 @@ export async function deployProject(
|
||||
logger.logDebug('Tagging images...');
|
||||
const taggedImages = await tagServiceImages(docker, images, serviceImages);
|
||||
try {
|
||||
const token = await getTokenForPreviousRepos(
|
||||
logger,
|
||||
appId,
|
||||
apiEndpoint,
|
||||
taggedImages,
|
||||
);
|
||||
await pushServiceImages(
|
||||
docker,
|
||||
logger,
|
||||
pineClient,
|
||||
taggedImages,
|
||||
token,
|
||||
skipLogUpload,
|
||||
);
|
||||
const { awaitInterruptibleTask } = await import('./helpers');
|
||||
// awaitInterruptibleTask throws SIGINTError on CTRL-C,
|
||||
// causing the release status to be set to 'failed'
|
||||
await awaitInterruptibleTask(async () => {
|
||||
const token = await getTokenForPreviousRepos(
|
||||
logger,
|
||||
appId,
|
||||
apiEndpoint,
|
||||
taggedImages,
|
||||
);
|
||||
await pushServiceImages(
|
||||
docker,
|
||||
logger,
|
||||
pineClient,
|
||||
taggedImages,
|
||||
token,
|
||||
skipLogUpload,
|
||||
);
|
||||
});
|
||||
release.status = 'success';
|
||||
} catch (err) {
|
||||
release.status = 'failed';
|
||||
@ -1597,22 +1659,11 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
|
||||
description:
|
||||
'Hide the image build log output (produce less verbose output)',
|
||||
}),
|
||||
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',
|
||||
}),
|
||||
'multi-dockerignore': flags.boolean({
|
||||
description:
|
||||
'Have each service use its own .dockerignore file. See "balena help build".',
|
||||
char: 'm',
|
||||
}),
|
||||
nogitignore: flags.boolean({
|
||||
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
|
||||
char: 'G',
|
||||
}),
|
||||
'noparent-check': flags.boolean({
|
||||
description:
|
||||
"Disable project validation check of 'docker-compose.yml' file in parent folder",
|
||||
@ -1622,17 +1673,14 @@ 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).",
|
||||
}),
|
||||
projectName: flags.string({
|
||||
description:
|
||||
'Specify an alternate project name; default is the directory name',
|
||||
description: stripIndent`\
|
||||
Name prefix for locally built images. This is the 'projectName' portion
|
||||
in 'projectName_serviceName:tag'. The default is the directory name.`,
|
||||
char: 'n',
|
||||
}),
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user