mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
270 Commits
v12.44.26
...
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 | |||
4c54d6c171 | |||
83f213c007 | |||
d0cdc900a2 | |||
9937b91606 | |||
972c2470c5 | |||
7d568a928b | |||
2331e0a3e5 | |||
cb9b6be24b | |||
c2d3eee7cc | |||
d8b08f7272 | |||
819bdac354 | |||
318de8f017 | |||
2b0341e12a | |||
21f7463607 | |||
19fd3094d1 | |||
7c4974f4f5 | |||
3b56ed278e | |||
254ef1c8cf | |||
d11f49e0f8 | |||
48d7d0ef5e | |||
c7bbbc4159 | |||
d2fabcaf30 | |||
e137c2aed2 | |||
58704b08d3 | |||
485a9e944f | |||
1d7a50f007 | |||
64a44e7a5f | |||
c3406603db | |||
f1fa187a58 | |||
6cb2893750 | |||
216172ed4f | |||
3717d8cc0f | |||
8338e2e933 | |||
918c2e912d | |||
be0622ec80 | |||
07eef7bb49 | |||
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
|
||||
|
10
.github/ISSUE_TEMPLATE.md
vendored
10
.github/ISSUE_TEMPLATE.md
vendored
@ -32,11 +32,11 @@ Please describe what actually happened instead:
|
||||
Examples:
|
||||
|
||||
```
|
||||
balena push myApp
|
||||
balena push myFleet
|
||||
balena push 192.168.0.12
|
||||
balena deploy myApp
|
||||
balena deploy myApp --build
|
||||
balena build . -a myApp
|
||||
balena deploy myFleet
|
||||
balena deploy myFleet --build
|
||||
balena build . -f myFleet
|
||||
balena build . -A armv7hf -d raspberrypi3
|
||||
```
|
||||
|
||||
@ -48,7 +48,7 @@ additional information. The `--logs` option reveals additional information for t
|
||||
|
||||
```
|
||||
balena build . --logs
|
||||
balena deploy myApp --build --logs
|
||||
balena deploy myFleet --build --logs
|
||||
```
|
||||
|
||||
# Steps to Reproduce the Problem
|
||||
|
@ -1,8 +1,10 @@
|
||||
module.exports = {
|
||||
spec: 'tests/commands/app/create.spec.ts',
|
||||
reporter: 'spec',
|
||||
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
944
CHANGELOG.md
944
CHANGELOG.md
@ -4,6 +4,950 @@ 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]
|
||||
* Fix env variable to avoid test failures [toochevere]
|
||||
* Add balena.yml handling and `--draft` to `balena deploy` release creation [toochevere]
|
||||
|
||||
## 12.47.0 - 2021-08-19
|
||||
|
||||
* Add deprecation policy checker and --unsupported global flag [Paulo Castro]
|
||||
|
||||
## 12.46.2 - 2021-08-16
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependencies (balena-sdk from v15.36.0 to v15.48.0) [Paulo Castro] </summary>
|
||||
|
||||
> ### 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]
|
||||
>
|
||||
> ### balena-sdk-15.44.0 - 2021-07-15
|
||||
>
|
||||
> * typings: Add the subscription.is_active computed term [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.43.0 - 2021-07-14
|
||||
>
|
||||
> * typings: Add the organization_memebership.effective_seat_role field [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.42.2 - 2021-07-14
|
||||
>
|
||||
> * tests: Reduce the number of organizations created [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.42.1 - 2021-07-13
|
||||
>
|
||||
> * tests/api-key: Fix a race condition in the apiKey.create() tests [Thodoris Greasidis]
|
||||
> * Convert the apiKey tests to async-await [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.42.0 - 2021-07-13
|
||||
>
|
||||
> * models/apiKey: Add getProvisioningApiKeysByApplication() method [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-15.41.1 - 2021-06-30
|
||||
>
|
||||
> * Delete CODEOWNERS [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.41.0 - 2021-06-21
|
||||
>
|
||||
> * Add organization__has_private_access_to__device_type typings [Thodoris Greasidis]
|
||||
> * typings: Add organization.has_past_due_invoice_since__date [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.40.0 - 2021-06-09
|
||||
>
|
||||
> * Add getAllNamedUserApiKeys() in the apiKey model [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.39.4 - 2021-06-08
|
||||
>
|
||||
> * Add missing modified_at in device type [JSReds]
|
||||
>
|
||||
> ### balena-sdk-15.39.3 - 2021-06-08
|
||||
>
|
||||
> * Fix lint with new linter version [JSReds]
|
||||
>
|
||||
> ### balena-sdk-15.39.2 - 2021-05-27
|
||||
>
|
||||
> * Update TypeScript to v4.3.2 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.39.1 - 2021-05-24
|
||||
>
|
||||
> * Update balena-lint to v6 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.39.0 - 2021-05-24
|
||||
>
|
||||
> * Add public device types [Tomás Migone]
|
||||
>
|
||||
> ### balena-sdk-15.38.0 - 2021-05-20
|
||||
>
|
||||
> * models/billing: Add changePlan method [Thodoris Greasidis]
|
||||
> * Update DOCUMENTATION about getAllWithDeviceServiceDetails deprecation [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.37.0 - 2021-05-17
|
||||
>
|
||||
> * Add public organization types [JSReds]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.46.1 - 2021-08-16
|
||||
|
||||
|
||||
<details>
|
||||
<summary> preload: Restore support for armv7 with custom preload image [Kyle Harding] </summary>
|
||||
|
||||
> ### balena-preload-10.5.0 - 2021-08-04
|
||||
>
|
||||
> * Remove mutually exclusive args from sfdisk [Kyle Harding]
|
||||
> * Explicitly disable tls to avoid startup delays [Kyle Harding]
|
||||
> * Use custom dind image based on alpine [Kyle Harding]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.46.0 - 2021-08-15
|
||||
|
||||
* Add `--draft` option to `balena push` [Felipe Lalanne]
|
||||
|
||||
## 12.45.2 - 2021-08-13
|
||||
|
||||
* push, build: Improve error handling (identify which service failed) [Paulo Castro]
|
||||
|
||||
## 12.45.1 - 2021-08-11
|
||||
|
||||
* envs, env add: Fix "Application is ambiguous" when using device UUID [Paulo Castro]
|
||||
|
||||
## 12.45.0 - 2021-08-09
|
||||
|
||||
* Rename applications to fleets (stage 1). See: https://git.io/JRuZr [Paulo Castro]
|
||||
|
||||
## 12.44.29 - 2021-07-26
|
||||
|
||||
* preload: Fix storage driver detection in balenaOS v2.80.9 [Kyle Harding]
|
||||
|
||||
## 12.44.28 - 2021-07-23
|
||||
|
||||
* os download: Improve error message for misspelled device type names [Paulo Castro]
|
||||
|
||||
## 12.44.27 - 2021-07-23
|
||||
|
||||
* docs: Add entry to FAQ/Troubleshooting for ETIMEDOUT with 'balena tunnel' [Paulo Castro]
|
||||
|
||||
## 12.44.26 - 2021-07-21
|
||||
|
||||
* config write: Fix EBUSY error on macOS [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.
|
||||
@ -259,3 +259,11 @@ gotchas to bear in mind:
|
||||
`node_modules/balena-sdk/node_modules/balena-errors`
|
||||
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
||||
`error.name === 'BalenaApplicationNotFound'`
|
||||
|
||||
## Further debugging notes
|
||||
|
||||
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
|
||||
|
||||
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
|
||||
|
22
README.md
22
README.md
@ -57,7 +57,7 @@ guide](https://docs.docker.com/compose/completion/) for system setup instruction
|
||||
## Logging in
|
||||
|
||||
Several CLI commands require access to your balenaCloud account, for example in order to push a
|
||||
new release to your application. Those commands require creating a CLI login session by running:
|
||||
new release to your fleet. Those commands require creating a CLI login session by running:
|
||||
|
||||
```sh
|
||||
$ balena login
|
||||
@ -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/).
|
||||
@ -156,14 +156,18 @@ of major, minor and patch version releases.
|
||||
|
||||
The latest release of a major version of the balena CLI will remain compatible with
|
||||
the balenaCloud backend services for at least one year from the date when the
|
||||
following major version is released. For example, balena CLI v10.17.5, as the
|
||||
latest v10 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v11.0.0 is released.
|
||||
following major version is released. For example, balena CLI v11.36.0, as the
|
||||
latest v11 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v12.0.0 was released.
|
||||
|
||||
At the end of this period, the older major version is considered deprecated and
|
||||
some of the functionality that depends on balenaCloud services may stop working
|
||||
at any time.
|
||||
Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
Half way through to that period (6 months after the release of the next major
|
||||
version), older major versions of the balena CLI will start printing a deprecation
|
||||
warning message when it is used interactively (when `stderr` is attached to a TTY
|
||||
device file). At the end of that period, older major versions will exit with an
|
||||
error message unless the `--unsupported` flag is used. This behavior was
|
||||
introduced in CLI version 12.47.0 and is also documented by `balena help`.
|
||||
To take advantage of the latest backend features and ensure compatibility, users
|
||||
are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
|
@ -31,6 +31,11 @@ command again.
|
||||
|
||||
Check whether the SD card is locked (a physical switch on the side of the card).
|
||||
|
||||
## I get `connect ETIMEDOUT` with `balena tunnel`
|
||||
|
||||
Please update the CLI to the latest version. This issue was fixed in v12.38.5.
|
||||
For more details, see: https://github.com/balena-io/balena-cli/issues/2172
|
||||
|
||||
## I get EINVAL errors on Cygwin
|
||||
|
||||
The errors may look something like this:
|
||||
|
@ -17,30 +17,31 @@
|
||||
|
||||
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';
|
||||
import * as filehound from 'filehound';
|
||||
import { Stats } from 'fs';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as klaw from 'klaw';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import * as util from 'util';
|
||||
import * as klaw from 'klaw';
|
||||
import { Stats } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { stripIndent } from '../lib/utils/lazy';
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import {
|
||||
diffLines,
|
||||
getSubprocessStdout,
|
||||
loadPackageJson,
|
||||
ROOT,
|
||||
StdOutTap,
|
||||
whichSpawn,
|
||||
} from './utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
@ -63,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 = {
|
||||
@ -95,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'
|
||||
@ -246,7 +248,17 @@ async function testPkg() {
|
||||
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
|
||||
// Run `balena version -j`, parse its stdout as JSON, and check that the
|
||||
// reported Node.js major version matches semver.major(process.version)
|
||||
const stdout = await getSubprocessStdout(pkgBalenaPath, ['version', '-j']);
|
||||
let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
|
||||
'version',
|
||||
'-j',
|
||||
]);
|
||||
const { filterCliOutputForTests } = await import('../tests/helpers');
|
||||
const filtered = filterCliOutputForTests({
|
||||
err: stderr.split(/\r?\n/),
|
||||
out: stdout.split(/\r?\n/),
|
||||
});
|
||||
stdout = filtered.out.join('\n');
|
||||
stderr = filtered.err.join('\n');
|
||||
let pkgNodeVersion = '';
|
||||
let pkgNodeMajorVersion = 0;
|
||||
try {
|
||||
@ -263,6 +275,10 @@ async function testPkg() {
|
||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
|
||||
);
|
||||
}
|
||||
if (filtered.err.length > 0) {
|
||||
const err = filtered.err.join('\n');
|
||||
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
|
||||
}
|
||||
console.log('Success! (standalone package test successful)');
|
||||
}
|
||||
|
||||
@ -295,7 +311,7 @@ async function zipPkg() {
|
||||
archive.on('warning', console.warn);
|
||||
|
||||
archive.pipe(outputStream);
|
||||
archive.finalize();
|
||||
archive.finalize().catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
@ -307,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')
|
||||
@ -411,8 +431,6 @@ async function renameInstallerFiles() {
|
||||
async function signWindowsInstaller() {
|
||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||
const exeName = renamedOclifInstallers[process.platform];
|
||||
const execFileAsync = util.promisify<string, string[], void>(execFile);
|
||||
|
||||
console.log(`Signing installer "${exeName}"`);
|
||||
await execFileAsync(MSYS2_BASH, [
|
||||
'sign-exe.sh',
|
||||
@ -443,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.
|
||||
@ -473,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));
|
||||
|
@ -34,15 +34,15 @@ const capitanoDoc = {
|
||||
files: ['build/commands/api-key/generate.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
title: 'Fleet',
|
||||
files: [
|
||||
'build/commands/apps.js',
|
||||
'build/commands/app/index.js',
|
||||
'build/commands/app/create.js',
|
||||
'build/commands/app/purge.js',
|
||||
'build/commands/app/rename.js',
|
||||
'build/commands/app/restart.js',
|
||||
'build/commands/app/rm.js',
|
||||
'build/commands/fleets.js',
|
||||
'build/commands/fleet/index.js',
|
||||
'build/commands/fleet/create.js',
|
||||
'build/commands/fleet/purge.js',
|
||||
'build/commands/fleet/rename.js',
|
||||
'build/commands/fleet/restart.js',
|
||||
'build/commands/fleet/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -75,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: [
|
||||
|
@ -58,7 +58,7 @@ class FakeHelpCommand {
|
||||
|
||||
examples = [
|
||||
'$ balena help',
|
||||
'$ balena help apps',
|
||||
'$ balena help login',
|
||||
'$ balena help os download',
|
||||
];
|
||||
|
||||
@ -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();
|
||||
|
@ -21,22 +21,6 @@ import * as path from 'path';
|
||||
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
const nodeEngineWarn = `\
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "v14.x.x" does not match required versions ">=10.20.0 <13.0.0".
|
||||
This may cause unexpected behavior. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`;
|
||||
const nodeEngineWarnArray = nodeEngineWarn.split('\n').filter((l) => l);
|
||||
|
||||
export function matchesNodeEngineVersionWarn(line: string) {
|
||||
line = line.replace(/"v14\.\d{1,3}\.\d{1,3}"/, '"v14.x.x"');
|
||||
return (
|
||||
line === nodeEngineWarn || nodeEngineWarnArray.includes(line.trimEnd())
|
||||
);
|
||||
}
|
||||
|
||||
/** Tap and buffer this process' stdout and stderr */
|
||||
export class StdOutTap {
|
||||
public stdoutBuf: string[] = [];
|
||||
@ -104,60 +88,6 @@ export function loadPackageJson() {
|
||||
return require(path.join(ROOT, 'package.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the executable at execPath as a child process, and resolve a promise
|
||||
* to the executable's stdout output as a string. Reject the promise if
|
||||
* anything is printed to stderr, or if the child process exits with a
|
||||
* non-zero exit code.
|
||||
* @param execPath Executable path
|
||||
* @param args Command-line argument for the executable
|
||||
*/
|
||||
export async function getSubprocessStdout(
|
||||
execPath: string,
|
||||
args: string[],
|
||||
): Promise<string> {
|
||||
const child = spawn(execPath, args);
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
child.stdout.on('error', reject);
|
||||
child.stderr.on('error', reject);
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
try {
|
||||
stdout = data.toString();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const stderr = data.toString();
|
||||
|
||||
// ignore any debug lines, but ensure that we parse
|
||||
// every line provided to the stderr stream
|
||||
const lines = _.filter(
|
||||
stderr.trim().split(/\r?\n/),
|
||||
(line) =>
|
||||
!line.startsWith('[debug]') && !matchesNodeEngineVersionWarn(line),
|
||||
);
|
||||
if (lines.length > 0) {
|
||||
reject(
|
||||
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.on('exit', (code: number) => {
|
||||
if (code) {
|
||||
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling wrapper around the npm `which` package:
|
||||
* "Like the unix which utility. Finds the first instance of a specified
|
||||
@ -187,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,18 +8,19 @@ _balena() {
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands=( apps build deploy envs 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 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 )
|
||||
env_cmds=( add rename rm )
|
||||
fleet_cmds=( create purge rename restart rm )
|
||||
internal_cmds=( osinit )
|
||||
key_cmds=( add rm )
|
||||
local_cmds=( configure flash )
|
||||
os_cmds=( build-config configure download initialize versions )
|
||||
release_cmds=( finalize )
|
||||
tag_cmds=( rm set )
|
||||
|
||||
|
||||
@ -42,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
|
||||
;;
|
||||
@ -57,6 +55,9 @@ _balena_sec_cmds() {
|
||||
"env")
|
||||
_describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0
|
||||
;;
|
||||
"fleet")
|
||||
_describe -t fleet_cmds 'fleet_cmd' fleet_cmds "$@" && ret=0
|
||||
;;
|
||||
"internal")
|
||||
_describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0
|
||||
;;
|
||||
@ -69,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,18 +7,19 @@ _balena_complete()
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands="apps build deploy envs 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 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"
|
||||
env_cmds="add rename rm"
|
||||
fleet_cmds="create purge rename restart rm"
|
||||
internal_cmds="osinit"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
release_cmds="finalize"
|
||||
tag_cmds="rm set"
|
||||
|
||||
|
||||
@ -36,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) )
|
||||
;;
|
||||
@ -51,6 +49,9 @@ _balena_complete()
|
||||
env)
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
fleet)
|
||||
COMPREPLY=( $(compgen -W "$fleet_cmds" -- $cur) )
|
||||
;;
|
||||
internal)
|
||||
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
|
||||
;;
|
||||
@ -63,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 --app APP_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. `APP_ID`. Create an application (`balena app create APP_NAME --type DEVICE_TYPE`) or find an existing one (`balena apps`) 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')),
|
||||
);
|
61
lib/app.ts
61
lib/app.ts
@ -16,8 +16,14 @@
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import {
|
||||
AppOptions,
|
||||
checkDeletedCommand,
|
||||
preparseArgs,
|
||||
unsupportedFlag,
|
||||
} from './preparser';
|
||||
import { CliSettings } from './utils/bootstrap';
|
||||
import { onceAsync, stripIndent } from './utils/lazy';
|
||||
import { onceAsync } from './utils/lazy';
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
@ -27,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,
|
||||
});
|
||||
@ -43,13 +50,8 @@ export const setupSentry = onceAsync(async () => {
|
||||
async function checkNodeVersion() {
|
||||
const validNodeVersions = packageJSON.engines.node;
|
||||
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behavior. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
const { getNodeEngineVersionWarn } = await import('./utils/messages');
|
||||
console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,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();
|
||||
|
||||
@ -89,14 +93,26 @@ 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: import('./preparser').AppOptions,
|
||||
) {
|
||||
async function oclifRun(command: string[], options: AppOptions) {
|
||||
let deprecationPromise: Promise<void>;
|
||||
// check and enforce the CLI's deprecation policy
|
||||
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||
deprecationPromise = Promise.resolve();
|
||||
} else {
|
||||
const { DeprecationChecker } = await import('./deprecation');
|
||||
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||
// warnAndAbortIfDeprecated uses previously cached data only
|
||||
await deprecationChecker.warnAndAbortIfDeprecated();
|
||||
// checkForNewReleasesIfNeeded may query the npm registry
|
||||
deprecationPromise = deprecationChecker.checkForNewReleasesIfNeeded();
|
||||
}
|
||||
|
||||
const runPromise = (async function (shouldFlush: boolean) {
|
||||
const { CustomMain } = await import('./utils/oclif-utils');
|
||||
let isEEXIT = false;
|
||||
@ -130,16 +146,17 @@ async function oclifRun(
|
||||
})(!options.noFlush);
|
||||
|
||||
const { trackPromise } = await import('./hooks/prerun/track');
|
||||
await Promise.all([trackPromise, runPromise]);
|
||||
|
||||
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
||||
}
|
||||
|
||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||
export async function run(
|
||||
cliArgs = process.argv,
|
||||
options: import('./preparser').AppOptions = {},
|
||||
) {
|
||||
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
|
||||
@ -150,8 +167,6 @@ export async function run(
|
||||
|
||||
await init();
|
||||
|
||||
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
|
||||
|
||||
// Look for commands that have been removed and if so, exit with a notice
|
||||
checkDeletedCommand(cliArgs.slice(2));
|
||||
|
||||
|
@ -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,96 +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 { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
device_count?: number;
|
||||
online_devices?: number;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
export default class AppsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all applications.
|
||||
|
||||
list all your balena applications.
|
||||
|
||||
For detailed information on a particular application,
|
||||
use \`balena app <application>\` instead.
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena apps'];
|
||||
|
||||
public static usage = 'apps';
|
||||
|
||||
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;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(AppsCmd);
|
||||
|
||||
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',
|
||||
'app_name',
|
||||
'slug',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import * as compose from '../utils/compose';
|
||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||
import {
|
||||
@ -33,7 +34,7 @@ import { dockerCliFlags } from '../utils/docker';
|
||||
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;
|
||||
}
|
||||
@ -51,7 +52,7 @@ the provided docker daemon in your development machine or balena device.
|
||||
(See also the \`balena push\` command for the option of building images in the
|
||||
balenaCloud build servers.)
|
||||
|
||||
You must provide either an application or a device-type/architecture pair.
|
||||
You must specify either a fleet, or the device type and architecture.
|
||||
|
||||
This command will look into the given source directory (or the current working
|
||||
directory if one isn't specified) for a docker-compose.yml file, and if found,
|
||||
@ -65,12 +66,12 @@ ${registrySecretsHelp}
|
||||
${dockerignoreHelp}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena build --application myApp',
|
||||
'$ balena build ./source/ --application myApp',
|
||||
'$ balena build --fleet myFleet',
|
||||
'$ balena build ./source/ --fleet myorg/myfleet',
|
||||
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
|
||||
'$ balena build --docker /var/run/docker.sock --application myApp # Linux, Mac',
|
||||
'$ balena build --docker //./pipe/docker_engine --application myApp # Windows',
|
||||
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -a myApp',
|
||||
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
|
||||
'$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows',
|
||||
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -91,10 +92,7 @@ ${dockerignoreHelp}
|
||||
description: 'the type of device this build is for',
|
||||
char: 'd',
|
||||
}),
|
||||
application: flags.string({
|
||||
description: 'name of the target balena application this build is for',
|
||||
char: 'a',
|
||||
}),
|
||||
fleet: cf.fleet,
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
@ -109,7 +107,7 @@ ${dockerignoreHelp}
|
||||
BuildCmd,
|
||||
);
|
||||
|
||||
await Command.checkLoggedInIf(!!options.application);
|
||||
await Command.checkLoggedInIf(!!options.fleet);
|
||||
|
||||
(await import('events')).defaultMaxListeners = 1000;
|
||||
|
||||
@ -153,14 +151,12 @@ ${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(
|
||||
'You must specify either an application or an arch/deviceType pair to build for',
|
||||
'You must specify either a fleet (-f), or the device type (-d) and architecture (-A)',
|
||||
);
|
||||
}
|
||||
|
||||
@ -181,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;
|
||||
@ -231,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 (
|
||||
@ -240,7 +241,7 @@ ${dockerignoreHelp}
|
||||
!appType.supports_multicontainer
|
||||
) {
|
||||
logger.logWarn(
|
||||
'Target application does not support multiple containers.\n' +
|
||||
'Target fleet does not support multiple containers.\n' +
|
||||
'Continuing with build, but you will not be able to deploy.',
|
||||
);
|
||||
}
|
||||
@ -258,7 +259,6 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
}
|
||||
|
@ -19,13 +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 } from '../../utils/messages';
|
||||
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;
|
||||
@ -36,6 +36,7 @@ interface FlagsDef {
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
appUpdatePollInterval?: string;
|
||||
'provisioning-key-name'?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -43,16 +44,17 @@ export default class ConfigGenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Generate a config.json file.
|
||||
|
||||
Generate a config.json file for a device or application.
|
||||
Generate a config.json file for a device or fleet.
|
||||
|
||||
Calling this command with the exact version number of the targeted image is required.
|
||||
The target balenaOS version must be specified with the --version option.
|
||||
|
||||
This command is interactive by default, but you can do this automatically without interactivity
|
||||
by specifying an option for each question on the command line, if you know the questions
|
||||
that will be asked for the relevant device type.
|
||||
${devModeInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
In case that you want to configure an image for an application with mixed device types,
|
||||
you can pass the --deviceType argument along with --application to specify the target device type.
|
||||
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.
|
||||
|
||||
To avoid interactive questions, specify a command line option for each question that
|
||||
would otherwise be asked.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
@ -60,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 --app MyApp --version 2.12.7',
|
||||
'$ balena config generate --app myorg/myapp --version 2.12.7',
|
||||
'$ balena config generate --app MyApp --version 2.12.7 --deviceType fincm3',
|
||||
'$ balena config generate --app MyApp --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --app MyApp --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1',
|
||||
'$ 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';
|
||||
@ -76,20 +77,20 @@ export default class ConfigGenerateCmd extends Command {
|
||||
description: 'a balenaOS version',
|
||||
required: true,
|
||||
}),
|
||||
application: { ...cf.application, exclusive: ['app', 'device'] },
|
||||
app: { ...cf.app, exclusive: ['application', 'device'] },
|
||||
device: flags.string({
|
||||
description: 'device uuid',
|
||||
char: 'd',
|
||||
exclusive: ['application', 'app'],
|
||||
}),
|
||||
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+',
|
||||
char: 'k',
|
||||
}),
|
||||
deviceType: flags.string({
|
||||
description: 'device type slug',
|
||||
description:
|
||||
"device type slug (run 'balena devices supported' for possible values)",
|
||||
}),
|
||||
'generate-device-api-key': flags.boolean({
|
||||
description: 'generate a fresh device key for the device',
|
||||
@ -113,7 +114,11 @@ export default class ConfigGenerateCmd extends Command {
|
||||
}),
|
||||
appUpdatePollInterval: flags.string({
|
||||
description:
|
||||
'how frequently (in minutes) to poll for application 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,
|
||||
};
|
||||
@ -143,8 +148,8 @@ export default class ConfigGenerateCmd extends Command {
|
||||
if (!rawDevice.belongs_to__application) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(stripIndent`
|
||||
Device ${options.device} does not appear to belong to an accessible application.
|
||||
Try with a different device, or use '--application' instead of '--device'.`);
|
||||
Device ${options.device} does not appear to belong to an accessible fleet.
|
||||
Try with a different device, or use '--fleet' instead of '--device'.`);
|
||||
}
|
||||
device = rawDevice as DeviceWithDeviceType & {
|
||||
belongs_to__application: PineDeferred;
|
||||
@ -152,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' },
|
||||
},
|
||||
@ -167,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,
|
||||
);
|
||||
@ -177,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 application ${options.application}`,
|
||||
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -186,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(
|
||||
@ -218,7 +225,7 @@ export default class ConfigGenerateCmd extends Command {
|
||||
}
|
||||
|
||||
protected readonly missingDeviceOrAppMessage = stripIndent`
|
||||
Either a device or an application must be specified.
|
||||
Either a device or a fleet must be specified.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
@ -226,21 +233,19 @@ export default class ConfigGenerateCmd extends Command {
|
||||
`;
|
||||
|
||||
protected readonly deviceTypeNotAllowedMessage =
|
||||
'The --deviceType option can only be used alongside the --application option';
|
||||
'The --deviceType option can only be used alongside the --fleet option';
|
||||
|
||||
protected async validateOptions(options: FlagsDef) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || 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');
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
registrySecretsHelp,
|
||||
buildArgDeprecation,
|
||||
} from '../utils/messages';
|
||||
import * as ca from '../utils/common-args';
|
||||
import * as compose from '../utils/compose';
|
||||
import type {
|
||||
BuiltImage,
|
||||
@ -33,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,
|
||||
@ -58,22 +59,23 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
build: boolean;
|
||||
nologupload: boolean;
|
||||
'release-tag'?: string[];
|
||||
draft: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
appName: string;
|
||||
fleet: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export default class DeployCmd extends Command {
|
||||
public static description = `\
|
||||
Deploy a single image or a multicontainer project to a balena application.
|
||||
Deploy a single image or a multicontainer project to a balena fleet.
|
||||
|
||||
Usage: \`deploy <appName> ([image] | --build [--source build-dir])\`
|
||||
Usage: \`deploy <fleet> ([image] | --build [--source build-dir])\`
|
||||
|
||||
Use this command to deploy an image or a complete multicontainer project to an
|
||||
application, optionally building it first. The source images are searched for
|
||||
Use this command to deploy an image or a complete multicontainer project to a
|
||||
fleet, optionally building it first. The source images are searched for
|
||||
(and optionally built) using the docker daemon in your development machine or
|
||||
balena device. (See also the \`balena push\` command for the option of building
|
||||
the image in the balenaCloud build servers.)
|
||||
@ -88,8 +90,8 @@ If a compose file isn't found, the command will look for a Dockerfile[.template]
|
||||
file (or alternative Dockerfile specified with the \`-f\` option), and if yet
|
||||
that isn't found, it will try to generate one.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
\`balena deploy <appOwnerUsername>/<appName>\`.
|
||||
To deploy to a fleet where you are a collaborator, use fleet slug including the
|
||||
organization: \`balena deploy <organization>/<fleet>\`.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
@ -97,25 +99,21 @@ ${dockerignoreHelp}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena deploy myApp',
|
||||
'$ balena deploy myApp --build --source myBuildDir/',
|
||||
'$ balena deploy myApp myApp/myImage',
|
||||
'$ balena deploy myApp myApp/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||
'$ balena deploy myFleet',
|
||||
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
|
||||
'$ balena deploy myorg/myfleet myRepo/myImage',
|
||||
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'appName',
|
||||
description: 'the name of the application to deploy to',
|
||||
required: true,
|
||||
},
|
||||
ca.fleetRequired,
|
||||
{
|
||||
name: 'image',
|
||||
description: 'the image to deploy',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'deploy <appName> [image]';
|
||||
public static usage = 'deploy <fleet> [image]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
source: flags.string({
|
||||
@ -139,6 +137,14 @@ ${dockerignoreHelp}
|
||||
`,
|
||||
multiple: true,
|
||||
}),
|
||||
draft: flags.boolean({
|
||||
description: stripIndent`
|
||||
Deploy the release as a draft. Draft releases are ignored
|
||||
by the 'track latest' release policy but can be used through release pinning.
|
||||
Draft releases can be marked as final through the API. Releases are created
|
||||
as final by default unless this option is given.`,
|
||||
default: false,
|
||||
}),
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
@ -160,7 +166,7 @@ ${dockerignoreHelp}
|
||||
const logger = await Command.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
const { appName, image } = params;
|
||||
const { fleet, image } = params;
|
||||
|
||||
// Build args are under consideration for removal - warn user
|
||||
if (options.buildArg) {
|
||||
@ -200,7 +206,7 @@ ${dockerignoreHelp}
|
||||
}
|
||||
|
||||
const helpers = await import('../utils/helpers');
|
||||
const app = await helpers.getAppWithArch(appName);
|
||||
const app = await helpers.getAppWithArch(fleet);
|
||||
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||
@ -211,11 +217,12 @@ ${dockerignoreHelp}
|
||||
|
||||
const release = await this.deployProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
appName, // may be prefixed by 'owner/', unlike app.app_name
|
||||
appName: fleet, // may be prefixed by 'owner/', unlike app.app_name
|
||||
image,
|
||||
shouldPerformBuild: !!options.build,
|
||||
shouldUploadLogs: !options.nologupload,
|
||||
buildEmulated: !!options.emulated,
|
||||
createAsDraft: options.draft,
|
||||
buildOpts,
|
||||
});
|
||||
await applyReleaseTagKeysAndValues(
|
||||
@ -238,7 +245,8 @@ ${dockerignoreHelp}
|
||||
shouldPerformBuild: boolean;
|
||||
shouldUploadLogs: boolean;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: any; // arguments to forward to docker build command
|
||||
buildOpts: BuildOpts;
|
||||
createAsDraft: boolean;
|
||||
},
|
||||
) {
|
||||
const _ = await import('lodash');
|
||||
@ -251,10 +259,15 @@ ${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 application does not support multiple containers. Aborting!',
|
||||
'Target fleet does not support multiple containers. Aborting!',
|
||||
);
|
||||
}
|
||||
|
||||
@ -306,7 +319,6 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
||||
@ -326,7 +338,7 @@ ${dockerignoreHelp}
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
|
||||
const msg = getChalk().yellow(
|
||||
'Target application requires legacy deploy method.',
|
||||
'Target fleet requires legacy deploy method.',
|
||||
);
|
||||
logger.logWarn(msg);
|
||||
|
||||
@ -370,6 +382,8 @@ ${dockerignoreHelp}
|
||||
`Bearer ${auth}`,
|
||||
apiEndpoint,
|
||||
!opts.shouldUploadLogs,
|
||||
composeOpts.projectPath,
|
||||
opts.createAsDraft,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
@ -23,10 +23,13 @@ import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { Application, Release } from 'balena-sdk';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
application_name?: string;
|
||||
fleet: string; // 'org/name' slug
|
||||
device_type?: string;
|
||||
commit?: string;
|
||||
last_seen?: string;
|
||||
@ -41,7 +44,7 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
undervoltage_detected?: boolean;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -70,13 +73,16 @@ export default class DeviceCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
@ -113,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;
|
||||
@ -162,37 +168,52 @@ export default class DeviceCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
'application_name',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
'cpu_usage_percent',
|
||||
'cpu_temp_c',
|
||||
'cpu_id',
|
||||
'memory_usage_mb',
|
||||
'memory_total_mb',
|
||||
'memory_usage_percent',
|
||||
'storage_block_device',
|
||||
'storage_usage_mb',
|
||||
'storage_total_mb',
|
||||
'storage_usage_percent',
|
||||
'undervoltage_detected',
|
||||
]),
|
||||
);
|
||||
const outputFields = [
|
||||
'device_name',
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
'fleet',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
'cpu_usage_percent',
|
||||
'cpu_temp_c',
|
||||
'cpu_id',
|
||||
'memory_usage_mb',
|
||||
'memory_total_mb',
|
||||
'memory_usage_percent',
|
||||
'storage_block_device',
|
||||
'storage_usage_mb',
|
||||
'storage_total_mb',
|
||||
'storage_usage_percent',
|
||||
'undervoltage_detected',
|
||||
];
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(device, outputFields, {
|
||||
...options,
|
||||
hideNullOrUndefinedValues: true,
|
||||
titleField: 'device_name',
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
outputFields.unshift(`$${device.device_name}$`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(
|
||||
device,
|
||||
outputFields.filter((f) => f !== 'device_name'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,40 +23,59 @@ import { applicationIdInfo } from '../../utils/messages';
|
||||
import { runCommand } from '../../utils/helpers';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
yes: boolean;
|
||||
advanced: boolean;
|
||||
'os-version'?: string;
|
||||
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 a certain application
|
||||
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:
|
||||
|
||||
Note, if the application 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 --application MyApp',
|
||||
'$ balena device init -a myorg/myapp',
|
||||
'$ 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> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
fleet: cf.fleet,
|
||||
yes: cf.yes,
|
||||
advanced: flags.boolean({
|
||||
char: 'v',
|
||||
@ -75,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,
|
||||
};
|
||||
|
||||
@ -95,14 +117,10 @@ export default class DeviceInitCmd extends Command {
|
||||
const logger = await Command.getLogger();
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
// Get application and
|
||||
const application = (await getApplication(
|
||||
balena,
|
||||
options['application'] ||
|
||||
options.fleet ||
|
||||
(
|
||||
await (await import('../../utils/patterns')).selectApplication()
|
||||
).id,
|
||||
@ -117,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);
|
||||
|
||||
@ -160,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);
|
||||
}
|
||||
|
||||
|
@ -15,22 +15,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
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 } from '../../utils/messages';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -40,12 +47,11 @@ interface ArgsDef {
|
||||
|
||||
export default class DeviceMoveCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Move one or more devices to another application.
|
||||
Move one or more devices to another fleet.
|
||||
|
||||
Move one or more devices to another application.
|
||||
Move one or more devices to another fleet.
|
||||
|
||||
Note, if the application option is omitted it will be prompted
|
||||
for interactively.
|
||||
If --fleet is omitted, the fleet will be prompted for interactively.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
@ -53,8 +59,8 @@ export default class DeviceMoveCmd extends Command {
|
||||
public static examples = [
|
||||
'$ balena device move 7cf02a6',
|
||||
'$ balena device move 7cf02a6,dc39e52',
|
||||
'$ balena device move 7cf02a6 --application MyNewApp',
|
||||
'$ balena device move 7cf02a6 -a myorg/mynewapp',
|
||||
'$ balena device move 7cf02a6 --fleet MyNewFleet',
|
||||
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
@ -69,8 +75,7 @@ export default class DeviceMoveCmd extends Command {
|
||||
public static usage = 'device move <uuid(s)>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
fleet: cf.fleet,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -84,10 +89,7 @@ export default class DeviceMoveCmd extends Command {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const { expandForAppName } = await import('../../utils/helpers');
|
||||
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
|
||||
|
||||
// Parse ids string into array of correct types
|
||||
const deviceIds: Array<string | number> = params.uuid
|
||||
@ -100,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';
|
||||
@ -118,17 +119,15 @@ 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
|
||||
for (const uuid of deviceIds) {
|
||||
try {
|
||||
await balena.models.device.move(uuid, application.id);
|
||||
console.info(
|
||||
`Device ${uuid} was moved to application ${application.slug}`,
|
||||
);
|
||||
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
|
||||
} catch (err) {
|
||||
console.info(`${err.message}, uuid: ${uuid}`);
|
||||
process.exitCode = 1;
|
||||
@ -140,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?`,
|
||||
);
|
||||
|
@ -31,10 +31,10 @@ interface ArgsDef {
|
||||
|
||||
export default class DevicePurgeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Purge application data from a device.
|
||||
Purge data from a device.
|
||||
|
||||
Purge application data from a device.
|
||||
This will clear the application's /data directory.
|
||||
Purge data from a device.
|
||||
This will clear the device's "/data" directory.
|
||||
|
||||
Multiple devices may be specified with a comma-separated list
|
||||
of values (no spaces).
|
||||
|
@ -29,27 +29,29 @@ interface FlagsDef {
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class DeviceRegisterCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Register a device.
|
||||
Register a new device.
|
||||
|
||||
Register a device to an application.
|
||||
Register a new device with a balena fleet.
|
||||
|
||||
If --uuid is not provided, a new UUID will be automatically assigned.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena device register MyApp',
|
||||
'$ balena device register MyApp --uuid <uuid>',
|
||||
'$ balena device register myorg/myapp --uuid <uuid>',
|
||||
'$ balena device register MyFleet',
|
||||
'$ balena device register MyFleet --uuid <uuid>',
|
||||
'$ balena device register myorg/myfleet --uuid <uuid>',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [ca.applicationRequired];
|
||||
public static args: Array<IArg<any>> = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'device register <application>';
|
||||
public static usage = 'device register <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
uuid: flags.string({
|
||||
@ -70,10 +72,10 @@ export default class DeviceRegisterCmd extends Command {
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const application = await getApplication(balena, params.application);
|
||||
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.
|
||||
@ -22,27 +22,29 @@ import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||
import type { Application } from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
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;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export default class DevicesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all devices.
|
||||
|
||||
list all devices that belong to you.
|
||||
List all of your devices.
|
||||
|
||||
You can filter the devices by application by using the \`--application\` option.
|
||||
Devices can be filtered by fleet with the \`--fleet\` option.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
@ -50,23 +52,19 @@ export default class DevicesCmd extends Command {
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices',
|
||||
'$ balena devices --application MyApp',
|
||||
'$ balena devices --app MyApp',
|
||||
'$ balena devices -a MyApp',
|
||||
'$ balena devices -a myorg/myapp',
|
||||
'$ balena devices --fleet MyFleet',
|
||||
'$ balena devices -f myorg/myfleet',
|
||||
];
|
||||
|
||||
public static usage = 'devices';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
json: cf.json,
|
||||
fleet: cf.fleet,
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
@ -74,15 +72,11 @@ export default class DevicesCmd extends Command {
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
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,
|
||||
@ -98,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);
|
||||
|
||||
@ -106,34 +100,52 @@ export default class DevicesCmd extends Command {
|
||||
return device;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'application_name',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
const _ = await import('lodash');
|
||||
if (options.json) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
devices.map((device) => _.pick(device, fields)),
|
||||
null,
|
||||
4,
|
||||
),
|
||||
);
|
||||
if (isV14()) {
|
||||
const outputFields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
|
||||
await this.outputData(devices, outputFields, {
|
||||
...options,
|
||||
displayNullValuesAs: 'N/a',
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
// Old output implementation
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -17,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
69
lib/commands/env/add.ts
vendored
69
lib/commands/env/add.ts
vendored
@ -24,7 +24,7 @@ import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string; // device UUID
|
||||
help: void;
|
||||
quiet: boolean;
|
||||
@ -38,18 +38,17 @@ interface ArgsDef {
|
||||
|
||||
export default class EnvAddCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Add env or config variable to application(s), device(s) or service(s).
|
||||
Add env or config variable to fleets, devices or services.
|
||||
|
||||
Add an environment or config variable to one or more applications, devices
|
||||
or services, as selected by the respective command-line options. Either the
|
||||
--application or the --device option must be provided, and either may be be
|
||||
Add an environment or config variable to one or more fleets, devices or
|
||||
services, as selected by the respective command-line options. Either the
|
||||
--fleet or the --device option must be provided, and either may be be
|
||||
used alongside the --service option to define a service-specific variable.
|
||||
(A service is an application container in a "microservices" application.)
|
||||
(A service corresponds to a Docker image/container in a microservices fleet.)
|
||||
When the --service option is used in conjunction with the --device option,
|
||||
the service variable applies to the selected device only. Otherwise, it
|
||||
applies to all devices of the selected application (i.e., the application's
|
||||
fleet). If the --service option is omitted, the variable applies to all
|
||||
services.
|
||||
the service variable applies to the selected device only. Otherwise, it
|
||||
applies to all devices of the selected fleet. If the --service option is
|
||||
omitted, the variable applies to all services.
|
||||
|
||||
If VALUE is omitted, the CLI will attempt to use the value of the environment
|
||||
variable of same name in the CLI process' environment. In this case, a warning
|
||||
@ -61,19 +60,18 @@ export default class EnvAddCmd extends Command {
|
||||
running on devices. They are also stored differently in the balenaCloud API
|
||||
database. Configuration variables cannot be set for specific services,
|
||||
therefore the --service option cannot be used when the variable name starts
|
||||
with a reserved prefix. When defining custom application variables, please
|
||||
avoid the reserved prefixes.
|
||||
with a reserved prefix. When defining custom fleet variables, please avoid
|
||||
these reserved prefixes.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena env add TERM --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp',
|
||||
'$ balena env add EDITOR vim -a myorg/myapp',
|
||||
'$ balena env add EDITOR vim --application MyApp,MyApp2',
|
||||
'$ balena env add EDITOR vim --application MyApp --service MyService',
|
||||
'$ balena env add EDITOR vim --application MyApp,MyApp2 --service MyService,MyService2',
|
||||
'$ balena env add TERM --fleet MyFleet',
|
||||
'$ balena env add EDITOR vim -f myorg/myfleet',
|
||||
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2',
|
||||
'$ balena env add EDITOR vim --fleet MyFleet --service MyService',
|
||||
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2 --service MyService,MyService2',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
|
||||
@ -97,8 +95,8 @@ export default class EnvAddCmd extends Command {
|
||||
public static usage = 'env add <name> [value]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: { ...cf.application, exclusive: ['device'] },
|
||||
device: { ...cf.device, exclusive: ['application'] },
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
device: { ...cf.device, exclusive: ['fleet'] },
|
||||
help: cf.help,
|
||||
quiet: cf.quiet,
|
||||
service: cf.service,
|
||||
@ -110,9 +108,9 @@ export default class EnvAddCmd extends Command {
|
||||
);
|
||||
const cmd = this;
|
||||
|
||||
if (!options.application && !options.device) {
|
||||
if (!options.fleet && !options.device) {
|
||||
throw new ExpectedError(
|
||||
'Either the --application or the --device option must be specified',
|
||||
'Either the --fleet or the --device option must be specified',
|
||||
);
|
||||
}
|
||||
|
||||
@ -152,16 +150,17 @@ 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,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, app: ${app}`);
|
||||
console.error(`${err.message}, fleet: ${app}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@ -183,15 +182,15 @@ export default class EnvAddCmd extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add service variables for a device or application.
|
||||
* Add service variables for a device or fleet.
|
||||
*/
|
||||
async function setServiceVars(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
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);
|
||||
@ -201,7 +200,7 @@ async function setServiceVars(
|
||||
params.value!,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, application: ${app}`);
|
||||
console.error(`${err.message}, fleet: ${app}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@ -216,7 +215,7 @@ async function setServiceVars(
|
||||
sdk,
|
||||
uuid,
|
||||
['id'],
|
||||
['app_name'],
|
||||
['slug'],
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, device: ${uuid}`);
|
||||
@ -225,11 +224,7 @@ async function setServiceVars(
|
||||
}
|
||||
for (const service of options.service!.split(',')) {
|
||||
try {
|
||||
const serviceId = await getServiceIdForApp(
|
||||
sdk,
|
||||
app.app_name,
|
||||
service,
|
||||
);
|
||||
const serviceId = await getServiceIdForApp(sdk, app.slug, service);
|
||||
await sdk.models.device.serviceVar.set(
|
||||
device.id,
|
||||
serviceId,
|
||||
@ -262,7 +257,7 @@ async function getServiceIdForApp(
|
||||
}
|
||||
if (serviceId === undefined) {
|
||||
throw new ExpectedError(
|
||||
`Cannot find service ${serviceName} for application ${appName}`,
|
||||
`Cannot find service ${serviceName} for fleet ${appName}`,
|
||||
);
|
||||
}
|
||||
return serviceId;
|
||||
|
4
lib/commands/env/rename.ts
vendored
4
lib/commands/env/rename.ts
vendored
@ -38,9 +38,9 @@ interface ArgsDef {
|
||||
|
||||
export default class EnvRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Change the value of a config or env var for an app, device or service.
|
||||
Change the value of a config or env var for a fleet, device or service.
|
||||
|
||||
Change the value of a configuration or environment variable for an application,
|
||||
Change the value of a configuration or environment variable for a fleet,
|
||||
device or service, as selected by command-line options.
|
||||
|
||||
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
|
||||
|
6
lib/commands/env/rm.ts
vendored
6
lib/commands/env/rm.ts
vendored
@ -37,9 +37,9 @@ interface ArgsDef {
|
||||
|
||||
export default class EnvRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a config or env var from an application, device or service.
|
||||
Remove a config or env var from a fleet, device or service.
|
||||
|
||||
Remove a configuration or environment variable from an application, device
|
||||
Remove a configuration or environment variable from a fleet, device
|
||||
or service, as selected by command-line options.
|
||||
|
||||
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
|
||||
@ -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-2019 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -22,54 +22,55 @@ import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import { isV13 } from '../utils/version';
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export default class EnvsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the environment or config variables of an application, device or service.
|
||||
List the environment or config variables of a fleet, device or service.
|
||||
|
||||
List the environment or configuration variables of an application, device or
|
||||
service, as selected by the respective command-line options. (A service is
|
||||
an application container in a "microservices" application.)
|
||||
List the environment or configuration variables of a fleet, device or
|
||||
service, as selected by the respective command-line options. (A service
|
||||
corresponds to a Docker image/container in a microservices fleet.)
|
||||
|
||||
The results include application-wide (fleet), device-wide (multiple services on
|
||||
a device) and service-specific variables that apply to the selected application,
|
||||
device or service. It can be thought of as including "inherited" variables;
|
||||
for example, a service inherits device-wide variables, and a device inherits
|
||||
application-wide variables.
|
||||
The results include fleet-wide (multiple devices), device-specific (multiple
|
||||
services on a specific device) and service-specific variables that apply to the
|
||||
selected fleet, device or service. It can be thought of as including inherited
|
||||
variables; for example, a service inherits device-wide variables, and a device
|
||||
inherits fleet-wide variables.
|
||||
|
||||
The printed output may include DEVICE and/or SERVICE columns to distinguish
|
||||
between application-wide, device-specific and service-specific variables.
|
||||
between fleet-wide, device-specific and service-specific variables.
|
||||
An asterisk in these columns indicates that the variable applies to
|
||||
"all devices" or "all services".
|
||||
|
||||
@ -83,22 +84,20 @@ export default class EnvsCmd extends Command {
|
||||
types like lists and empty strings. The 'jq' utility may be helpful in shell
|
||||
scripts (https://stedolan.github.io/jq/manual/). When --json is used, an empty
|
||||
JSON array ([]) is printed instead of an error message when no variables exist
|
||||
for the given query. When querying variables for a device, note that the
|
||||
application name may be null in JSON output (or 'N/A' in tabular output) if the
|
||||
application linked to the device is no longer accessible by the current user
|
||||
(for example, in case the current user has been removed from the application
|
||||
by its owner).
|
||||
for the given query. When querying variables for a device, note that the fleet
|
||||
name may be null in JSON output (or 'N/A' in tabular output) if the fleet that
|
||||
the device belonged to is no longer accessible by the current user (for example,
|
||||
in case the current user was removed from the fleet by the fleet's owner).
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena envs --application MyApp',
|
||||
'$ balena envs --application myorg/myapp',
|
||||
'$ balena envs --application MyApp --json',
|
||||
'$ balena envs --application MyApp --service MyService',
|
||||
'$ balena envs --application MyApp --service MyService',
|
||||
'$ balena envs --application MyApp --config',
|
||||
'$ balena envs --fleet myorg/myfleet',
|
||||
'$ balena envs --fleet MyFleet --json',
|
||||
'$ balena envs --fleet MyFleet --service MyService',
|
||||
'$ balena envs --fleet MyFleet --service MyService',
|
||||
'$ balena envs --fleet MyFleet --config',
|
||||
'$ balena envs --device 7cf02a6',
|
||||
'$ balena envs --device 7cf02a6 --json',
|
||||
'$ balena envs --device 7cf02a6 --config --json',
|
||||
@ -108,44 +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: stripIndent`
|
||||
No-op since balena CLI v12.0.0.`,
|
||||
hidden: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
application: { exclusive: ['device'], ...cf.application },
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
config: flags.boolean({
|
||||
default: false,
|
||||
char: 'c',
|
||||
description: 'show configuration variables only',
|
||||
exclusive: ['service'],
|
||||
}),
|
||||
device: { exclusive: ['application'], ...cf.device },
|
||||
device: { ...cf.device, exclusive: ['fleet'] },
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
verbose: cf.verbose,
|
||||
service: { exclusive: ['config'], ...cf.service },
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
service: { ...cf.service, exclusive: ['config'] },
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
|
||||
|
||||
const variables: EnvironmentVariableInfo[] = [];
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
if (!options.application && !options.device) {
|
||||
throw new ExpectedError('You must specify an application or 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) {
|
||||
@ -154,27 +144,27 @@ export default class EnvsCmd extends Command {
|
||||
balena,
|
||||
options.device,
|
||||
['uuid'],
|
||||
['app_name'],
|
||||
['slug'],
|
||||
);
|
||||
fullUUID = device.uuid;
|
||||
if (app) {
|
||||
appNameOrSlug = app.app_name;
|
||||
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
|
||||
? `application "${options.application}"`
|
||||
(options.fleet
|
||||
? `fleet "${options.fleet}"`
|
||||
: `device "${options.device}"`);
|
||||
throw new ExpectedError(`No environment variables found for ${target}`);
|
||||
}
|
||||
@ -186,33 +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) => {
|
||||
i.appName = i.appName || (options.json ? null : 'N/A');
|
||||
i.fleet ||= options.json ? null : 'N/A';
|
||||
return i;
|
||||
});
|
||||
|
||||
fields.push(options.json ? 'appName' : 'appName => APPLICATION');
|
||||
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) {
|
||||
this.log(
|
||||
stringifyVarArray<SDK.EnvironmentVariableBase>(varArray, fields),
|
||||
);
|
||||
// Rename fields
|
||||
if (options.device) {
|
||||
if (options.json) {
|
||||
fields.push('deviceUUID');
|
||||
} else {
|
||||
results.forEach((r: any) => {
|
||||
r.device = r.deviceUUID;
|
||||
delete r.deviceUUID;
|
||||
});
|
||||
|
||||
fields.push('device');
|
||||
}
|
||||
}
|
||||
if (!options.config) {
|
||||
if (options.json) {
|
||||
fields.push('serviceName');
|
||||
} else {
|
||||
results.forEach((r: any) => {
|
||||
r.service = r.serviceName;
|
||||
delete r.serviceName;
|
||||
});
|
||||
fields.push('service');
|
||||
}
|
||||
}
|
||||
|
||||
await this.outputData(results, fields, {
|
||||
...options,
|
||||
sort: options.sort || 'name',
|
||||
});
|
||||
} else {
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
// Old output implementation
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
if (!options.config) {
|
||||
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../utils/helpers');
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -220,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 application "${appName}"`,
|
||||
`Service "${serviceName}" not found for fleet "${fleetSlug}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -241,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> = {
|
||||
@ -267,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;
|
||||
@ -283,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!;
|
||||
@ -292,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> = {
|
||||
@ -313,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;
|
||||
@ -335,7 +359,7 @@ function fillInInfoFields(
|
||||
| EnvironmentVariableInfo[]
|
||||
| DeviceServiceEnvironmentVariableInfo[]
|
||||
| ServiceEnvironmentVariableInfo[],
|
||||
appNameOrSlug?: string,
|
||||
fleetSlug?: string,
|
||||
deviceUUID?: string,
|
||||
) {
|
||||
for (const envVar of varArray) {
|
||||
@ -349,30 +373,8 @@ function fillInInfoFields(
|
||||
?.installs__service as SDK.Service[]
|
||||
)[0]?.service_name;
|
||||
}
|
||||
envVar.appName = appNameOrSlug;
|
||||
envVar.fleet = fleetSlug;
|
||||
envVar.serviceName = envVar.serviceName || '*';
|
||||
envVar.deviceUUID = deviceUUID || '*';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform each object (item) of varArray to preserve only the
|
||||
* fields (keys) listed in the fields argument.
|
||||
*/
|
||||
function stringifyVarArray<T = Dictionary<any>>(
|
||||
varArray: T[],
|
||||
fields: string[],
|
||||
): string {
|
||||
const transformed = varArray.map((o: Dictionary<any>) =>
|
||||
_.transform(
|
||||
o,
|
||||
(result, value, key) => {
|
||||
if (fields.includes(key)) {
|
||||
result[key] = value;
|
||||
}
|
||||
},
|
||||
{} as Dictionary<any>,
|
||||
),
|
||||
);
|
||||
return JSON.stringify(transformed, null, 4);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 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.
|
||||
@ -16,11 +16,12 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
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 type { Application } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
@ -32,18 +33,18 @@ interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppCreateCmd extends Command {
|
||||
export default class FleetCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create an application.
|
||||
Create a fleet.
|
||||
|
||||
Create a new balena application.
|
||||
Create a new balena fleet.
|
||||
|
||||
You can specify the organization the application should belong to using
|
||||
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 application's default device type is specified with the \`--type\` option.
|
||||
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.
|
||||
|
||||
@ -55,31 +56,30 @@ export default class AppCreateCmd extends Command {
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena app create MyApp',
|
||||
'$ balena app create MyApp --organization mmyorg',
|
||||
'$ balena app create MyApp -o myorg --type raspberry-pi',
|
||||
'$ 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: 'application name',
|
||||
description: 'fleet name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app create <name>';
|
||||
public static usage = 'fleet create <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
organization: flags.string({
|
||||
char: 'o',
|
||||
description:
|
||||
'handle of the organization the application should belong to',
|
||||
description: 'handle of the organization the fleet should belong to',
|
||||
}),
|
||||
type: flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'application device type (Check available types with `balena devices supported`)',
|
||||
'fleet device type (Check available types with `balena devices supported`)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
@ -88,7 +88,7 @@ export default class AppCreateCmd extends Command {
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppCreateCmd,
|
||||
FleetCreateCmd,
|
||||
);
|
||||
|
||||
// Ascertain device type
|
||||
@ -112,12 +112,12 @@ export default class AppCreateCmd extends Command {
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
throw new ExpectedError(
|
||||
`Error: application "${params.name}" already exists in organization "${organization}".`,
|
||||
`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 applications in organization "${organization}".`,
|
||||
`Error: You are not authorized to create fleets in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -125,11 +125,8 @@ export default class AppCreateCmd extends Command {
|
||||
}
|
||||
|
||||
// Output
|
||||
const { isV13 } = await import('../../utils/version');
|
||||
console.log(
|
||||
isV13()
|
||||
? `Application created: slug "${application.slug}", device type "${deviceType}"`
|
||||
: `Application created: ${application.slug} (${deviceType}, id ${application.id})`,
|
||||
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 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,58 +15,63 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
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 } from '../../utils/messages';
|
||||
import type { Release } from 'balena-sdk';
|
||||
import { isV14 } from '../../utils/version';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class AppCmd extends Command {
|
||||
export default class FleetCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display information about a single application.
|
||||
Display information about a single fleet.
|
||||
|
||||
Display detailed information about a single balena application.
|
||||
Display detailed information about a single fleet.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = ['$ balena app MyApp', '$ balena app myorg/myapp'];
|
||||
public static examples = [
|
||||
'$ balena fleet MyFleet',
|
||||
'$ balena fleet myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.applicationRequired];
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'app <nameOrSlug>';
|
||||
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 } = this.parse<FlagsDef, ArgsDef>(AppCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
FleetCmd,
|
||||
);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const application = (await getApplication(
|
||||
getBalenaSdk(),
|
||||
params.application,
|
||||
{
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
},
|
||||
const application = (await getApplication(getBalenaSdk(), params.fleet, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
},
|
||||
)) as ApplicationWithDeviceType & {
|
||||
})) as ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
// For display purposes:
|
||||
device_type: string;
|
||||
@ -76,15 +81,23 @@ export default class AppCmd extends Command {
|
||||
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',
|
||||
]),
|
||||
);
|
||||
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',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
@ -27,27 +28,27 @@ interface FlagsDef {
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class AppPurgeCmd extends Command {
|
||||
export default class FleetPurgeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Purge data from an application.
|
||||
Purge data from a fleet.
|
||||
|
||||
Purge data from all devices belonging to an application.
|
||||
This will clear the application's /data directory.
|
||||
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 app purge MyApp',
|
||||
'$ balena app purge myorg/myapp',
|
||||
'$ balena fleet purge MyFleet',
|
||||
'$ balena fleet purge myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.applicationRequired];
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'app purge <application>';
|
||||
public static usage = 'fleet purge <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
@ -56,7 +57,7 @@ export default class AppPurgeCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
@ -64,7 +65,7 @@ export default class AppPurgeCmd extends Command {
|
||||
|
||||
// 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.application);
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
try {
|
||||
await balena.models.application.purge(application.id);
|
@ -15,28 +15,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
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 } from '../../utils/messages';
|
||||
import type { ApplicationType } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
newName?: string;
|
||||
}
|
||||
|
||||
export default class AppRenameCmd extends Command {
|
||||
export default class FleetRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Rename an application.
|
||||
Rename a fleet.
|
||||
|
||||
Rename an application.
|
||||
Rename a fleet.
|
||||
|
||||
Note, if the \`newName\` parameter is omitted, it will be
|
||||
prompted for interactively.
|
||||
@ -45,20 +46,20 @@ export default class AppRenameCmd extends Command {
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena app rename OldName',
|
||||
'$ balena app rename OldName NewName',
|
||||
'$ balena app rename myorg/oldname NewName',
|
||||
'$ balena fleet rename OldName',
|
||||
'$ balena fleet rename OldName NewName',
|
||||
'$ balena fleet rename myorg/oldname NewName',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
ca.applicationRequired,
|
||||
ca.fleetRequired,
|
||||
{
|
||||
name: 'newName',
|
||||
description: 'the new name for the application',
|
||||
description: 'the new name for the fleet',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app rename <application> [newName]';
|
||||
public static usage = 'fleet rename <fleet> [newName]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
@ -67,7 +68,7 @@ export default class AppRenameCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
|
||||
|
||||
const { validateApplicationName } = await import('../../utils/validation');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
@ -76,7 +77,7 @@ export default class AppRenameCmd extends Command {
|
||||
|
||||
// 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.application, {
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: ['is_legacy'],
|
||||
@ -86,16 +87,14 @@ export default class AppRenameCmd extends Command {
|
||||
|
||||
// Check app exists
|
||||
if (!application) {
|
||||
throw new ExpectedError(
|
||||
'Error: application ${params.nameOrSlug} not found.',
|
||||
);
|
||||
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(
|
||||
`Application ${params.application} is of 'legacy' type, and cannot be renamed.`,
|
||||
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -103,20 +102,31 @@ export default class AppRenameCmd extends Command {
|
||||
const newName =
|
||||
params.newName ||
|
||||
(await getCliForm().ask({
|
||||
message: 'Please enter the new name for this application:',
|
||||
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: application ${params.application} already exists.`,
|
||||
`Error: new fleet name may only include characters [a-zA-Z0-9_-].`,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
@ -128,7 +138,7 @@ export default class AppRenameCmd extends Command {
|
||||
);
|
||||
|
||||
// Output result
|
||||
console.log(`Application renamed`);
|
||||
console.log(`Fleet renamed`);
|
||||
console.log('From:');
|
||||
console.log(`\tname: ${application.app_name}`);
|
||||
console.log(`\tslug: ${application.slug}`);
|
@ -15,7 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
@ -27,26 +28,26 @@ interface FlagsDef {
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class AppRestartCmd extends Command {
|
||||
export default class FleetRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart an application.
|
||||
Restart a fleet.
|
||||
|
||||
Restart all devices belonging to an application.
|
||||
Restart all devices belonging to a fleet.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena app restart MyApp',
|
||||
'$ balena app restart myorg/myapp',
|
||||
'$ balena fleet restart MyFleet',
|
||||
'$ balena fleet restart myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.applicationRequired];
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'app restart <application>';
|
||||
public static usage = 'fleet restart <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
@ -55,14 +56,14 @@ export default class AppRestartCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
||||
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.application);
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
await balena.models.application.restart(application.id);
|
||||
}
|
@ -15,7 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
@ -28,14 +29,14 @@ interface FlagsDef {
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class AppRmCmd extends Command {
|
||||
export default class FleetRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove an application.
|
||||
Remove a fleet.
|
||||
|
||||
Permanently remove a balena application.
|
||||
Permanently remove a fleet.
|
||||
|
||||
The --yes option may be used to avoid interactive confirmation.
|
||||
|
||||
@ -43,14 +44,14 @@ export default class AppRmCmd extends Command {
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena app rm MyApp',
|
||||
'$ balena app rm MyApp --yes',
|
||||
'$ balena app rm myorg/myapp',
|
||||
'$ balena fleet rm MyFleet',
|
||||
'$ balena fleet rm MyFleet --yes',
|
||||
'$ balena fleet rm myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [ca.applicationRequired];
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'app rm <application>';
|
||||
public static usage = 'fleet rm <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
yes: cf.yes,
|
||||
@ -61,7 +62,7 @@ export default class AppRmCmd extends Command {
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppRmCmd,
|
||||
FleetRmCmd,
|
||||
);
|
||||
|
||||
const { confirm } = await import('../../utils/patterns');
|
||||
@ -71,11 +72,11 @@ export default class AppRmCmd extends Command {
|
||||
// Confirm
|
||||
await confirm(
|
||||
options.yes ?? false,
|
||||
`Are you sure you want to delete application ${params.application}?`,
|
||||
`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.application);
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
// Remove
|
||||
await balena.models.application.remove(application.id);
|
108
lib/commands/fleets.ts
Normal file
108
lib/commands/fleets.ts
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @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 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);
|
||||
|
@ -23,7 +23,7 @@ import { applicationIdInfo } from '../utils/messages';
|
||||
import { parseAsLocalHostnameOrIp } from '../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
pollInterval?: number;
|
||||
help?: void;
|
||||
}
|
||||
@ -34,22 +34,22 @@ interface ArgsDef {
|
||||
|
||||
export default class JoinCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Move a local device to an application on another balena server.
|
||||
Move a local device to a fleet on another balena server.
|
||||
|
||||
Move a local device to an application on another balena server, causing
|
||||
Move a local device to a fleet on another balena server, causing
|
||||
the device to "join" the new server. The device must be running balenaOS.
|
||||
|
||||
For example, you could provision a device against an openBalena installation
|
||||
where you perform end-to-end tests and then move it to balenaCloud when it's
|
||||
ready for production.
|
||||
|
||||
To move a device between applications on the same server, use the
|
||||
To move a device between fleets on the same server, use the
|
||||
\`balena device move\` command instead of \`balena join\`.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This may require administrator/root privileges.
|
||||
Likewise, if the application flag is not provided then a picker will be shown.
|
||||
Likewise, if the fleet option is not provided then a picker will be shown.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
@ -57,10 +57,10 @@ export default class JoinCmd extends Command {
|
||||
public static examples = [
|
||||
'$ balena join',
|
||||
'$ balena join balena.local',
|
||||
'$ balena join balena.local --application MyApp',
|
||||
'$ balena join balena.local -a myorg/myapp',
|
||||
'$ balena join balena.local --fleet MyFleet',
|
||||
'$ balena join balena.local -f myorg/myfleet',
|
||||
'$ balena join 192.168.1.25',
|
||||
'$ balena join 192.168.1.25 --application MyApp',
|
||||
'$ balena join 192.168.1.25 --fleet MyFleet',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -75,7 +75,7 @@ export default class JoinCmd extends Command {
|
||||
public static usage = 'join [deviceIpOrHostname]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
fleet: cf.fleet,
|
||||
pollInterval: flags.integer({
|
||||
description: 'the interval in minutes to check for updates',
|
||||
char: 'i',
|
||||
@ -98,7 +98,7 @@ export default class JoinCmd extends Command {
|
||||
logger,
|
||||
sdk,
|
||||
params.deviceIpOrHostname,
|
||||
options.application,
|
||||
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']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,9 +31,9 @@ interface ArgsDef {
|
||||
|
||||
export default class LeaveCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a local device from its balena application.
|
||||
Remove a local device from its balena fleet.
|
||||
|
||||
Remove a local device from its balena application, causing the device to
|
||||
Remove a local device from its balena fleet, causing the device to
|
||||
"leave" the server it is provisioned on. This effectively makes the device
|
||||
"unmanaged". The device must be running balenaOS.
|
||||
|
||||
|
@ -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,26 +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 } from '../../utils/messages';
|
||||
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 {
|
||||
@ -51,23 +51,21 @@ 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.
|
||||
|
||||
Configure a previously downloaded balenaOS image for a specific device type or
|
||||
balena application.
|
||||
Configure a previously downloaded balenaOS image for a specific device type
|
||||
or fleet.
|
||||
|
||||
Configuration settings such as WiFi authentication will be taken from the
|
||||
following sources, in precedence order:
|
||||
@ -75,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 application's default
|
||||
device type, in case of an application 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
|
||||
@ -97,12 +95,10 @@ 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 --app MyApp',
|
||||
'$ balena os configure ../path/rpi3.img -a myorg/myapp',
|
||||
'$ balena os configure ../path/rpi3.img --app MyApp --version 2.12.7',
|
||||
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3',
|
||||
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3 --config myWifiConfig.json',
|
||||
'$ 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',
|
||||
'$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3 --config myWifiConfig.json',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -121,15 +117,15 @@ export default class OsConfigureCmd extends Command {
|
||||
description:
|
||||
'ask advanced configuration questions (when in interactive mode)',
|
||||
}),
|
||||
application: { ...cf.application, exclusive: ['app', 'device'] },
|
||||
app: { ...cf.app, exclusive: ['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:
|
||||
'interval (in minutes) for the on-device balena supervisor periodic app update check',
|
||||
'supervisor cloud polling interval in minutes (e.g. for variable updates)',
|
||||
}),
|
||||
'config-network': flags.string({
|
||||
description: 'device network type (non-interactive configuration)',
|
||||
@ -141,15 +137,11 @@ export default class OsConfigureCmd extends Command {
|
||||
'config-wifi-ssid': flags.string({
|
||||
description: 'WiFi SSID (network name) (non-interactive configuration)',
|
||||
}),
|
||||
device: { exclusive: ['app', 'application'], ...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 application device type',
|
||||
'device type slug (e.g. "raspberrypi3") to override the fleet device type',
|
||||
}),
|
||||
'initial-device-name': flags.string({
|
||||
description:
|
||||
@ -165,16 +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,
|
||||
);
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
await validateOptions(options);
|
||||
|
||||
@ -201,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' },
|
||||
},
|
||||
@ -222,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);
|
||||
}
|
||||
@ -280,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');
|
||||
|
||||
@ -300,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 '--application' option must be provided",
|
||||
"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 '--application' option",
|
||||
"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();
|
||||
}
|
||||
@ -366,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 application ${options.application}`,
|
||||
`Device type ${options['device-type']} is incompatible with fleet ${options.fleet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -391,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,8 +98,49 @@ 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');
|
||||
|
||||
await downloadOSImage(params.type, options.output, options.version);
|
||||
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'));
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import {
|
||||
getBalenaSdk,
|
||||
@ -27,20 +27,15 @@ import {
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import type { DockerConnectionCliFlags } from '../utils/docker';
|
||||
import { dockerConnectionCliFlags } from '../utils/docker';
|
||||
import * as _ from 'lodash';
|
||||
import type {
|
||||
Application,
|
||||
BalenaSDK,
|
||||
DeviceTypeJson,
|
||||
PineExpand,
|
||||
Release,
|
||||
} from 'balena-sdk';
|
||||
import type { Preloader } from 'balena-preload';
|
||||
import { parseAsInteger } from '../utils/validation';
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import * as _ from 'lodash';
|
||||
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;
|
||||
'dont-check-arch': boolean;
|
||||
@ -56,16 +51,16 @@ interface ArgsDef {
|
||||
|
||||
export default class PreloadCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Preload an app on a disk image (or Edison zip archive).
|
||||
Preload a release on a disk image (or Edison zip archive).
|
||||
|
||||
Preload a balena application release (app images/containers), and optionally
|
||||
Preload a release (service images/containers) from a balena fleet, and optionally
|
||||
a balenaOS splash screen, in a previously downloaded '.img' balenaOS image file
|
||||
in the local disk (a zip file is only accepted for the Intel Edison device type).
|
||||
After preloading, the balenaOS image file can be flashed to a device's SD card.
|
||||
When the device boots, it will not need to download the application, as it was
|
||||
When the device boots, it will not need to download the release, as it was
|
||||
preloaded. This is usually combined with release pinning
|
||||
(https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/)
|
||||
to avoid the device dowloading a newer release straight away, if one is available.
|
||||
to avoid the device downloading a newer release straight away, if available.
|
||||
Check also the Preloading and Preregistering section of the balena CLI's advanced
|
||||
masterclass document:
|
||||
https://www.balena.io/docs/learn/more/masterclasses/advanced-cli/#5-preloading-and-preregistering
|
||||
@ -82,8 +77,8 @@ export default class PreloadCmd extends Command {
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena preload balena.img --app MyApp --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0',
|
||||
'$ balena preload balena.img --app myorg/myapp --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png',
|
||||
'$ balena preload balena.img --fleet MyFleet --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0',
|
||||
'$ balena preload balena.img --fleet myorg/myfleet --splash-image image.png',
|
||||
'$ balena preload balena.img',
|
||||
];
|
||||
|
||||
@ -98,13 +93,15 @@ export default class PreloadCmd extends Command {
|
||||
public static usage = 'preload <image>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
// TODO: Replace with application/a in #v13?
|
||||
app: cf.application,
|
||||
fleet: cf.fleet,
|
||||
commit: flags.string({
|
||||
description: `\
|
||||
The commit hash for a specific application release to preload, use "current" to specify the current
|
||||
release (ignored if no appId is given). The current release is usually also the latest, but can be
|
||||
manually pinned using https://github.com/balena-io-projects/staged-releases .\
|
||||
The commit hash of the release to preload. Use "current" to specify the current
|
||||
release (ignored if no appId is given). The current release is usually also the
|
||||
latest, but can be pinned to a specific release. See:
|
||||
https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/
|
||||
https://www.balena.io/docs/learn/more/masterclasses/fleet-management/#63-pin-using-the-api
|
||||
https://github.com/balena-io-examples/staged-releases\
|
||||
`,
|
||||
char: 'c',
|
||||
}),
|
||||
@ -115,7 +112,7 @@ manually pinned using https://github.com/balena-io-projects/staged-releases .\
|
||||
'dont-check-arch': flags.boolean({
|
||||
default: false,
|
||||
description:
|
||||
'disables check for matching architecture in image and application',
|
||||
'disable architecture compatibility check between image and fleet',
|
||||
}),
|
||||
'pin-device-to-release': flags.boolean({
|
||||
default: false,
|
||||
@ -185,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(`Application 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'];
|
||||
@ -229,17 +220,14 @@ 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 an application if you disable the architecture check.',
|
||||
'You need to specify a fleet if you disable the architecture check.',
|
||||
);
|
||||
}
|
||||
|
||||
@ -256,7 +244,7 @@ Can be repeated to add multiple certificates.\
|
||||
const preloader = new balenaPreload.Preloader(
|
||||
null,
|
||||
docker,
|
||||
appId,
|
||||
fleetSlug,
|
||||
commit,
|
||||
image,
|
||||
splashImage,
|
||||
@ -273,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;
|
||||
}
|
||||
});
|
||||
@ -293,7 +288,7 @@ Can be repeated to add multiple certificates.\
|
||||
preloader.on('error', reject);
|
||||
resolve(
|
||||
this.prepareAndPreload(preloader, balena, {
|
||||
appId,
|
||||
appId: fleetSlug,
|
||||
commit,
|
||||
pinDevice,
|
||||
}),
|
||||
@ -316,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'],
|
||||
@ -330,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) {
|
||||
@ -417,14 +409,14 @@ Can be repeated to add multiple certificates.\
|
||||
applicationInfoSpinner.stop();
|
||||
if (applications.length === 0) {
|
||||
throw new ExpectedError(
|
||||
`You have no apps with successful releases for a '${deviceTypeSlug}' device type.`,
|
||||
`No fleets found with successful releases for device type '${deviceTypeSlug}'`,
|
||||
);
|
||||
}
|
||||
return getCliForm().ask({
|
||||
message: 'Select an application',
|
||||
message: 'Select a fleet',
|
||||
type: 'list',
|
||||
choices: applications.map((app) => ({
|
||||
name: app.app_name,
|
||||
name: app.slug,
|
||||
value: app,
|
||||
})),
|
||||
});
|
||||
@ -432,7 +424,7 @@ Can be repeated to add multiple certificates.\
|
||||
|
||||
selectApplicationCommit(releases: Release[]) {
|
||||
if (releases.length === 0) {
|
||||
throw new ExpectedError('This application has no successful releases.');
|
||||
throw new ExpectedError('This fleet has no successful releases.');
|
||||
}
|
||||
const DEFAULT_CHOICE = { name: 'current', value: 'current' };
|
||||
const choices = [DEFAULT_CHOICE].concat(
|
||||
@ -465,23 +457,23 @@ Can be repeated to add multiple certificates.\
|
||||
}
|
||||
const message = `\
|
||||
|
||||
This application is set to track the latest release, and non-pinned devices
|
||||
This fleet is set to track the latest release, and non-pinned devices
|
||||
are automatically updated when a new release is available. This may lead to
|
||||
unexpected behavior: The preloaded device will download and install the latest
|
||||
release once it is online.
|
||||
|
||||
This prompt gives you the opportunity to disable automatic updates for this
|
||||
application now. Note that this would result in the application being pinned
|
||||
to the current latest release, rather than some other release that may have
|
||||
This prompt gives you the opportunity to disable automatic updates for
|
||||
this fleet now. Note that this would result in the fleet being pinned to
|
||||
the current latest release, rather than some other release that may have
|
||||
been selected for preloading. The pinned released may be further managed
|
||||
through the web dashboard or programatically through the balena API / SDK.
|
||||
Documentation about release policies and app/device pinning can be found at:
|
||||
Documentation about release policies and pinning can be found at:
|
||||
https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/
|
||||
|
||||
Alternatively, the --pin-device-to-release flag may be used to pin only the
|
||||
preloaded device to the selected release.
|
||||
|
||||
Would you like to disable automatic updates for this application now?\
|
||||
Would you like to disable automatic updates for this fleet now?\
|
||||
`;
|
||||
const update = await getCliForm().ask({
|
||||
message,
|
||||
@ -499,7 +491,7 @@ Would you like to disable automatic updates for this application 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, {
|
||||
@ -531,7 +523,7 @@ Would you like to disable automatic updates for this application now?\
|
||||
if (this.isCurrentCommit(options.commit)) {
|
||||
if (!appCommit) {
|
||||
throw new Error(
|
||||
`Unexpected empty commit hash for app ID "${application.id}"`,
|
||||
`Unexpected empty commit hash for fleet ID "${application.id}"`,
|
||||
);
|
||||
}
|
||||
// handle `--commit current` (and its `--commit latest` synonym)
|
||||
|
@ -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 {
|
||||
@ -36,7 +35,7 @@ enum BuildTarget {
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
applicationOrDevice: string;
|
||||
fleetOrDevice: string;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
@ -47,25 +46,23 @@ 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[];
|
||||
draft: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class PushCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Start a build on the remote balenaCloud build servers, or a local mode device.
|
||||
Build release images on balenaCloud servers or on a local mode device.
|
||||
|
||||
Start a build on the remote balenaCloud build servers, or a local mode device.
|
||||
Build release images on balenaCloud servers or on a local mode device.
|
||||
|
||||
When building on the balenaCloud servers, the given source directory will be
|
||||
sent to the remote server. This can be used as a drop-in replacement for the
|
||||
@ -92,16 +89,16 @@ export default class PushCmd extends Command {
|
||||
|
||||
${dockerignoreHelp.split('\n').join('\n\t\t')}
|
||||
|
||||
Note: the --service and --env flags must come after the applicationOrDevice
|
||||
Note: the --service and --env flags must come after the fleetOrDevice
|
||||
parameter, as per examples.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena push myApp',
|
||||
'$ balena push myApp --source <source directory>',
|
||||
'$ balena push myApp -s <source directory>',
|
||||
'$ balena push myApp --release-tag key1 "" key2 "value2 with spaces"',
|
||||
'$ balena push myorg/myapp',
|
||||
'$ balena push myFleet',
|
||||
'$ balena push myFleet --source <source directory>',
|
||||
'$ balena push myFleet -s <source directory>',
|
||||
'$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"',
|
||||
'$ balena push myorg/myfleet',
|
||||
'',
|
||||
'$ balena push 10.0.0.1',
|
||||
'$ balena push 10.0.0.1 --source <source directory>',
|
||||
@ -115,15 +112,15 @@ export default class PushCmd extends Command {
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'applicationOrDevice',
|
||||
name: 'fleetOrDevice',
|
||||
description:
|
||||
'application name or slug, or local device IP address or hostname',
|
||||
'fleet name or slug, or local device IP address or ".local" hostname',
|
||||
required: true,
|
||||
parse: lowercaseIfSlug,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'push <applicationOrDevice>';
|
||||
public static usage = 'push <fleetOrDevice>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
source: flags.string({
|
||||
@ -188,7 +185,7 @@ export default class PushCmd extends Command {
|
||||
When pushing to the cloud, this option will cause the build to start, then
|
||||
return execution back to the shell, with the status and release ID (if
|
||||
applicable). When pushing to a local mode device, this option will cause
|
||||
the command to not tail application logs when the build has completed.`,
|
||||
the command to not tail logs when the build has completed.`,
|
||||
char: 'd',
|
||||
default: false,
|
||||
}),
|
||||
@ -217,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,
|
||||
@ -236,37 +223,24 @@ 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`
|
||||
Set release tags if the push to a cloud application is successful. Multiple
|
||||
Set release tags if the image build is successful (balenaCloud only). Multiple
|
||||
arguments may be provided, alternating tag keys and values (see examples).
|
||||
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
||||
`,
|
||||
multiple: true,
|
||||
exclusive: ['detached'],
|
||||
}),
|
||||
draft: flags.boolean({
|
||||
description: stripIndent`
|
||||
Instruct the builder to create the release as a draft. Draft releases are ignored
|
||||
by the 'track latest' release policy but can be used through release pinning.
|
||||
Draft releases can be marked as final through the API. Releases are created
|
||||
as final by default unless this option is given.`,
|
||||
default: false,
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -292,14 +266,12 @@ export default class PushCmd extends Command {
|
||||
},
|
||||
);
|
||||
|
||||
switch (await this.getBuildTarget(params.applicationOrDevice)) {
|
||||
switch (await this.getBuildTarget(params.fleetOrDevice)) {
|
||||
case BuildTarget.Cloud:
|
||||
logger.logDebug(
|
||||
`Pushing to cloud for application: ${params.applicationOrDevice}`,
|
||||
);
|
||||
logger.logDebug(`Pushing to cloud for fleet: ${params.fleetOrDevice}`);
|
||||
|
||||
await this.pushToCloud(
|
||||
params.applicationOrDevice,
|
||||
params.fleetOrDevice,
|
||||
options,
|
||||
sdk,
|
||||
dockerfilePath,
|
||||
@ -308,11 +280,9 @@ export default class PushCmd extends Command {
|
||||
break;
|
||||
|
||||
case BuildTarget.Device:
|
||||
logger.logDebug(
|
||||
`Pushing to local device: ${params.applicationOrDevice}`,
|
||||
);
|
||||
logger.logDebug(`Pushing to local device: ${params.fleetOrDevice}`);
|
||||
await this.pushToDevice(
|
||||
params.applicationOrDevice,
|
||||
params.fleetOrDevice,
|
||||
options,
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
@ -366,13 +336,13 @@ export default class PushCmd extends Command {
|
||||
registrySecrets,
|
||||
headless: options.detached,
|
||||
convertEol: !options['noconvert-eol'],
|
||||
isDraft: options.draft,
|
||||
};
|
||||
const args = {
|
||||
appSlug: application.slug,
|
||||
source: options.source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
nogitignore: !options.gitignore,
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
@ -398,11 +368,11 @@ export default class PushCmd extends Command {
|
||||
registrySecrets: RegistrySecrets,
|
||||
) {
|
||||
// Check for invalid options
|
||||
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag'];
|
||||
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag', 'draft'];
|
||||
this.checkInvalidOptions(
|
||||
remoteOnlyOptions,
|
||||
options,
|
||||
'is only valid when pushing to an application',
|
||||
'is only valid when pushing to a fleet',
|
||||
);
|
||||
|
||||
const deviceDeploy = await import('../utils/device/deploy');
|
||||
@ -416,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,20 +31,20 @@ interface FlagsDef {
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
applicationOrDevice: string;
|
||||
fleetOrDevice: string;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
export default class SshCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
SSH into the host or application container of a device.
|
||||
Open a SSH prompt on a device's host OS or service container.
|
||||
|
||||
Start a shell on a local or remote device. If a service name is not provided,
|
||||
a shell will be opened on the host OS.
|
||||
|
||||
If an application is provided, an interactive menu will be presented
|
||||
for the selection of an online device. A shell will then be opened for the
|
||||
host OS or service container of the chosen device.
|
||||
If a fleet is provided, an interactive menu will be presented for the selection
|
||||
of an online device. A shell will then be opened for the host OS or service
|
||||
container of the chosen device.
|
||||
|
||||
For local devices, the IP address and .local domain name are supported.
|
||||
If the device is referenced by IP or \`.local\` address, the connection
|
||||
@ -64,7 +64,7 @@ export default class SshCmd extends Command {
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena ssh MyApp',
|
||||
'$ balena ssh MyFleet',
|
||||
'$ balena ssh f49cefd',
|
||||
'$ balena ssh f49cefd my-service',
|
||||
'$ balena ssh f49cefd --port <port>',
|
||||
@ -76,9 +76,9 @@ export default class SshCmd extends Command {
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'applicationOrDevice',
|
||||
name: 'fleetOrDevice',
|
||||
description:
|
||||
'application name/slug/id, device uuid, or address of local device',
|
||||
'fleet name/slug/id, device uuid, or address of local device',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
@ -88,7 +88,7 @@ export default class SshCmd extends Command {
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'ssh <applicationOrDevice> [service]';
|
||||
public static usage = 'ssh <fleetOrDevice> [service]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
port: flags.integer({
|
||||
@ -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>(
|
||||
@ -124,10 +125,10 @@ export default class SshCmd extends Command {
|
||||
);
|
||||
|
||||
// Local connection
|
||||
if (validateLocalHostnameOrIp(params.applicationOrDevice)) {
|
||||
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
|
||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||
return await performLocalDeviceSSH({
|
||||
address: params.applicationOrDevice,
|
||||
address: params.fleetOrDevice,
|
||||
port: options.port,
|
||||
forceTTY: options.tty,
|
||||
verbose: options.verbose,
|
||||
@ -144,10 +145,11 @@ 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,
|
||||
params.applicationOrDevice,
|
||||
params.fleetOrDevice,
|
||||
);
|
||||
|
||||
const device = await sdk.models.device.get(deviceUuid, {
|
||||
|
@ -23,7 +23,7 @@ import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
duration?: string;
|
||||
help: void;
|
||||
@ -35,16 +35,16 @@ interface ArgsDef {
|
||||
|
||||
export default class SupportCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Grant or revoke support access for devices and applications.
|
||||
Grant or revoke support access for devices or fleets.
|
||||
|
||||
Grant or revoke balena support agent access to devices and applications
|
||||
Grant or revoke balena support agent access to devices or fleets
|
||||
on balenaCloud. (This command does not apply to openBalena.)
|
||||
Access will be automatically revoked once the specified duration has elapsed.
|
||||
|
||||
Duration defaults to 24h, but can be specified using --duration flag in days
|
||||
or hours, e.g. '12h', '2d'.
|
||||
|
||||
Both --device and --application flags accept multiple values, specified as
|
||||
Both --device and --fleet flags accept multiple values, specified as
|
||||
a comma-separated list (with no spaces).
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
@ -52,8 +52,8 @@ export default class SupportCmd extends Command {
|
||||
|
||||
public static examples = [
|
||||
'balena support enable --device ab346f,cd457a --duration 3d',
|
||||
'balena support enable --application app3 --duration 12h',
|
||||
'balena support disable -a myorg/myapp',
|
||||
'balena support enable --fleet myFleet --duration 12h',
|
||||
'balena support disable -f myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -71,10 +71,10 @@ export default class SupportCmd extends Command {
|
||||
description: 'comma-separated list (no spaces) of device UUIDs',
|
||||
char: 'd',
|
||||
}),
|
||||
application: {
|
||||
...cf.application,
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
description:
|
||||
'comma-separated list (no spaces) of application names or org/name slugs',
|
||||
'comma-separated list (no spaces) of fleet names or slugs (preferred)',
|
||||
},
|
||||
duration: flags.string({
|
||||
description:
|
||||
@ -97,10 +97,8 @@ export default class SupportCmd extends Command {
|
||||
const enabling = params.action === 'enable';
|
||||
|
||||
// Validation
|
||||
if (!options.device && !options.application) {
|
||||
throw new ExpectedError(
|
||||
'At least one device or application must be specified',
|
||||
);
|
||||
if (!options.device && !options.fleet) {
|
||||
throw new ExpectedError('At least one device or fleet must be specified');
|
||||
}
|
||||
|
||||
if (options.duration != null && !enabling) {
|
||||
@ -115,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';
|
||||
@ -132,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} application ${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} application ${appName}`);
|
||||
await balena.models.application.revokeSupportAccess(appName);
|
||||
ux.action.start(`${disablingMessage} fleet ${slug}`);
|
||||
await balena.models.application.revokeSupportAccess(slug);
|
||||
}
|
||||
ux.action.stop();
|
||||
}
|
||||
|
@ -22,11 +22,10 @@ import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -35,16 +34,16 @@ interface ArgsDef {
|
||||
|
||||
export default class TagRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a tag from an application, device or release.
|
||||
Remove a tag from a fleet, device or release.
|
||||
|
||||
Remove a tag from an application, device or release.
|
||||
Remove a tag from a fleet, device or release.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena tag rm myTagKey --application MyApp',
|
||||
'$ balena tag rm myTagKey -a myorg/myapp',
|
||||
'$ balena tag rm myTagKey --fleet MyFleet',
|
||||
'$ balena tag rm myTagKey -f myorg/myfleet',
|
||||
'$ balena tag rm myTagKey --device 7cf02a6',
|
||||
'$ balena tag rm myTagKey --release 1234',
|
||||
'$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6',
|
||||
@ -61,21 +60,17 @@ export default class TagRmCmd extends Command {
|
||||
public static usage = 'tag rm <tagKey>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'device', 'release'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'device', 'release'],
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
exclusive: ['fleet', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
exclusive: ['fleet', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
@ -87,24 +82,20 @@ export default class TagRmCmd extends Command {
|
||||
TagRmCmd,
|
||||
);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
@ -130,9 +121,9 @@ export default class TagRmCmd extends Command {
|
||||
protected static missingResourceMessage = stripIndent`
|
||||
To remove a resource tag, you must provide exactly one of:
|
||||
|
||||
* An application, with --application <appNameOrSlug>
|
||||
* A device, with --device <uuid>
|
||||
* A release, with --release <id or commit>
|
||||
* A fleet, with --fleet <fleetNameOrSlug>
|
||||
* A device, with --device <UUID>
|
||||
* A release, with --release <ID or commit>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
|
@ -22,11 +22,10 @@ import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -36,9 +35,9 @@ interface ArgsDef {
|
||||
|
||||
export default class TagSetCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Set a tag on an application, device or release.
|
||||
Set a tag on a fleet, device or release.
|
||||
|
||||
Set a tag on an application, device or release.
|
||||
Set a tag on a fleet, device or release.
|
||||
|
||||
You can optionally provide a value to be associated with the created
|
||||
tag, as an extra argument after the tag key. If a value isn't
|
||||
@ -48,9 +47,9 @@ export default class TagSetCmd extends Command {
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena tag set mySimpleTag --application MyApp',
|
||||
'$ balena tag set mySimpleTag -a myorg/myapp',
|
||||
'$ balena tag set myCompositeTag myTagValue --application MyApp',
|
||||
'$ balena tag set mySimpleTag --fleet MyFleet',
|
||||
'$ balena tag set mySimpleTag -f myorg/myfleet',
|
||||
'$ balena tag set myCompositeTag myTagValue --fleet MyFleet',
|
||||
'$ balena tag set myCompositeTag myTagValue --device 7cf02a6',
|
||||
'$ balena tag set myCompositeTag "my tag value with whitespaces" --device 7cf02a6',
|
||||
'$ balena tag set myCompositeTag myTagValue --release 1234',
|
||||
@ -74,21 +73,17 @@ export default class TagSetCmd extends Command {
|
||||
public static usage = 'tag set <tagKey> [value]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'device', 'release'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'device', 'release'],
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
exclusive: ['fleet', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
exclusive: ['fleet', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
@ -100,14 +95,10 @@ export default class TagSetCmd extends Command {
|
||||
TagSetCmd,
|
||||
);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
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);
|
||||
}
|
||||
@ -116,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,
|
||||
);
|
||||
@ -151,9 +142,9 @@ export default class TagSetCmd extends Command {
|
||||
protected static missingResourceMessage = stripIndent`
|
||||
To set a resource tag, you must provide exactly one of:
|
||||
|
||||
* An application, with --application <appNameOrSlug>
|
||||
* A device, with --device <uuid>
|
||||
* A release, with --release <id or commit>
|
||||
* A fleet, with --fleet <fleetNameOrSlug>
|
||||
* A device, with --device <UUID>
|
||||
* A release, with --release <ID or commit>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
|
@ -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,28 +21,30 @@ import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import type { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
export default class TagsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all tags for an application, device or release.
|
||||
List all tags for a fleet, device or release.
|
||||
|
||||
List all tags and their values for a particular application,
|
||||
device or release.
|
||||
List all tags and their values for the specified fleet, device or release.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena tags --application MyApp',
|
||||
'$ balena tags -a myorg/myapp',
|
||||
'$ balena tags --fleet MyFleet',
|
||||
'$ balena tags -f myorg/myfleet',
|
||||
'$ balena tags --device 7cf02a6',
|
||||
'$ balena tags --release 1234',
|
||||
'$ balena tags --release b376b0e544e9429483b656490e5b9443b4349bd6',
|
||||
@ -51,22 +53,19 @@ export default class TagsCmd extends Command {
|
||||
public static usage = 'tags';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'device', 'release'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'device', 'release'],
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
exclusive: ['fleet', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
exclusive: ['fleet', 'device'],
|
||||
},
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -75,25 +74,21 @@ export default class TagsCmd extends Command {
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
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) {
|
||||
@ -113,17 +108,23 @@ 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`
|
||||
To list tags for a resource, you must provide exactly one of:
|
||||
|
||||
* An application, with --application <appNameOrSlug>
|
||||
* A fleet, with --fleet <fleetNameOrSlug>
|
||||
* A device, with --device <uuid>
|
||||
* A release, with --release <id or commit>
|
||||
|
||||
|
@ -24,6 +24,8 @@ import {
|
||||
} from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { lowercaseIfSlug } from '../utils/normalization';
|
||||
|
||||
import type { Server, Socket } from 'net';
|
||||
|
||||
interface FlagsDef {
|
||||
@ -32,7 +34,7 @@ interface FlagsDef {
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
deviceOrApplication: string;
|
||||
deviceOrFleet: string;
|
||||
}
|
||||
|
||||
export default class TunnelCmd extends Command {
|
||||
@ -62,7 +64,7 @@ export default class TunnelCmd extends Command {
|
||||
|
||||
public static examples = [
|
||||
'# map remote port 22222 to localhost:22222',
|
||||
'$ balena tunnel myApp -p 22222',
|
||||
'$ balena tunnel myFleet -p 22222',
|
||||
'',
|
||||
'# map remote port 22222 to localhost:222',
|
||||
'$ balena tunnel 2ead211 -p 22222:222',
|
||||
@ -71,21 +73,22 @@ export default class TunnelCmd extends Command {
|
||||
'$ balena tunnel 1546690 -p 22222:0.0.0.0',
|
||||
'',
|
||||
'# map remote port 22222 to any address on your host machine, port 222',
|
||||
'$ balena tunnel myApp -p 22222:0.0.0.0:222',
|
||||
'$ balena tunnel myFleet -p 22222:0.0.0.0:222',
|
||||
'',
|
||||
'# multiple port tunnels can be specified at any one time',
|
||||
'$ balena tunnel myApp -p 8080:3000 -p 8081:9000',
|
||||
'$ balena tunnel myFleet -p 8080:3000 -p 8081:9000',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'deviceOrApplication',
|
||||
description: 'device uuid or application name/slug/id',
|
||||
name: 'deviceOrFleet',
|
||||
description: 'device UUID or fleet name/slug/ID',
|
||||
required: true,
|
||||
parse: lowercaseIfSlug,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'tunnel <deviceOrApplication>';
|
||||
public static usage = 'tunnel <deviceOrFleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
port: flags.string({
|
||||
@ -132,10 +135,7 @@ export default class TunnelCmd extends Command {
|
||||
|
||||
// Ascertain device uuid
|
||||
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
|
||||
const uuid = await getOnlineTargetDeviceUuid(
|
||||
sdk,
|
||||
params.deviceOrApplication,
|
||||
);
|
||||
const uuid = await getOnlineTargetDeviceUuid(sdk, params.deviceOrFleet);
|
||||
const device = await sdk.models.device.get(uuid);
|
||||
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
|
||||
|
||||
|
@ -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,
|
||||
|
234
lib/deprecation.ts
Normal file
234
lib/deprecation.ts
Normal file
@ -0,0 +1,234 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
export interface ReleaseTimestampsByVersion {
|
||||
[version: string]: string; // e.g. { '12.0.0': '2021-06-16T12:54:52.000Z' }
|
||||
lastFetched: string; // ISO 8601 timestamp, e.g. '2021-06-27T16:46:10.000Z'
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn about and enforce the CLI deprecation policy stated in the README
|
||||
* file. In particular:
|
||||
* The latest release of a major version will remain compatible with
|
||||
* the backend services for at least one year from the date when the
|
||||
* following major version is released. [...]
|
||||
* Half way through to that period (6 months), old major versions of the
|
||||
* balena CLI will start printing a deprecation warning message.
|
||||
* At the end of that period, older major versions will abort with an error
|
||||
* message unless the `--unsupported` flag is used.
|
||||
*
|
||||
* - Check for new balena-cli releases by querying the npm registry.
|
||||
* - Cache results for a number of days to improve performance.
|
||||
*
|
||||
* For this feature's specification and planning, see (restricted access):
|
||||
* https://jel.ly.fish/ed8d2395-9323-418c-bb67-d11d32a17d00
|
||||
*/
|
||||
export class DeprecationChecker {
|
||||
readonly majorVersionFetchIntervalDays = 7;
|
||||
readonly expiryDays = 365;
|
||||
readonly deprecationDays = Math.ceil(this.expiryDays / 2);
|
||||
readonly msInDay = 24 * 60 * 60 * 1000; // milliseconds in a day
|
||||
readonly debugPrefix = 'Deprecation check';
|
||||
readonly cacheFile = 'cachedReleaseTimestamps';
|
||||
readonly now = new Date().getTime();
|
||||
private initialized = false;
|
||||
storage: ReturnType<typeof import('balena-settings-storage')>;
|
||||
cachedTimestamps: ReleaseTimestampsByVersion;
|
||||
nextMajorVersion: string; // semver without the 'v' prefix
|
||||
|
||||
constructor(protected currentVersion: string) {
|
||||
const semver = require('semver') as typeof import('semver');
|
||||
const major = semver.major(this.currentVersion, { loose: true });
|
||||
this.nextMajorVersion = `${major + 1}.0.0`;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
this.initialized = true;
|
||||
|
||||
const settings = await import('balena-settings-client');
|
||||
const getStorage = await import('balena-settings-storage');
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
this.storage = getStorage({ dataDirectory });
|
||||
let stored: ReleaseTimestampsByVersion | undefined;
|
||||
try {
|
||||
stored = (await this.storage.get(
|
||||
this.cacheFile,
|
||||
)) as ReleaseTimestampsByVersion;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
this.cachedTimestamps = {
|
||||
...stored,
|
||||
// '1970-01-01T00:00:00.000Z' is new Date(0).toISOString()
|
||||
lastFetched: stored?.lastFetched || '1970-01-01T00:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPM registry URL to retrieve the package.json file for a given version.
|
||||
* @param version Semver without 'v' prefix, e.g. '12.0.0.'
|
||||
*/
|
||||
protected getNpmUrl(version: string) {
|
||||
return `http://registry.npmjs.org/balena-cli/${version}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the npm registry (HTTP request) for a given balena-cli version.
|
||||
*
|
||||
* @param version semver version without the 'v' prefix, e.g. '13.0.0'
|
||||
* @returns `undefined` if the request status code is 404 (version not
|
||||
* published), otherwise a publishedAt date in ISO 8601 format, e.g.
|
||||
* '2021-06-27T16:46:10.000Z'.
|
||||
*/
|
||||
protected async fetchPublishedTimestampForVersion(
|
||||
version: string,
|
||||
): Promise<string | undefined> {
|
||||
const { default: got } = await import('got');
|
||||
const url = this.getNpmUrl(version);
|
||||
let response: import('got').Response<Dictionary<any>> | undefined;
|
||||
try {
|
||||
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) {
|
||||
throw new Error(`Failed to query "${url}":\n${e}`);
|
||||
}
|
||||
}
|
||||
// response.body looks like a package.json file, plus possibly a
|
||||
// `versionist.publishedAt` field added by `github.com/product-os/versionist`
|
||||
const publishedAt: string | undefined =
|
||||
response?.body?.versionist?.publishedAt;
|
||||
if (!publishedAt && process.env.DEBUG) {
|
||||
console.error(`\
|
||||
[debug] ${this.debugPrefix}: balena CLI next major version "${this.nextMajorVersion}" not released, \
|
||||
or release date not available`);
|
||||
}
|
||||
return publishedAt; // ISO 8601, e.g. '2021-06-27T16:46:10.000Z'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we already know (cached value) when the next major version
|
||||
* was released. If we don't know, check how long ago the npm registry
|
||||
* was last fetched, and fetch again if it has been longer than
|
||||
* `majorVersionFetchIntervalDays`.
|
||||
*/
|
||||
public async checkForNewReleasesIfNeeded() {
|
||||
if (process.env.BALENARC_UNSUPPORTED) {
|
||||
return; // for the benefit of code testing
|
||||
}
|
||||
await this.init();
|
||||
if (this.cachedTimestamps[this.nextMajorVersion]) {
|
||||
// A cached value exists: no need to check the npm registry
|
||||
return;
|
||||
}
|
||||
const lastFetched = new Date(this.cachedTimestamps.lastFetched).getTime();
|
||||
const daysSinceLastFetch = (this.now - lastFetched) / this.msInDay;
|
||||
if (daysSinceLastFetch < this.majorVersionFetchIntervalDays) {
|
||||
if (process.env.DEBUG) {
|
||||
// toFixed(5) results in a precision of ~1 second
|
||||
const days = daysSinceLastFetch.toFixed(5);
|
||||
console.error(`\
|
||||
[debug] ${this.debugPrefix}: ${days} days since last npm registry query for next major version release date.
|
||||
[debug] Will not query the registry again until at least ${this.majorVersionFetchIntervalDays} days have passed.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`\
|
||||
[debug] ${
|
||||
this.debugPrefix
|
||||
}: Cache miss for the balena CLI next major version release date.
|
||||
[debug] Will query ${this.getNpmUrl(this.nextMajorVersion)}`);
|
||||
}
|
||||
try {
|
||||
const publishedAt = await this.fetchPublishedTimestampForVersion(
|
||||
this.nextMajorVersion,
|
||||
);
|
||||
if (publishedAt) {
|
||||
this.cachedTimestamps[this.nextMajorVersion] = publishedAt;
|
||||
}
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] ${this.debugPrefix}: ${e}`);
|
||||
}
|
||||
}
|
||||
// Refresh `lastFetched` regardless of whether or not the request to the npm
|
||||
// registry was successful. Will try again after `majorVersionFetchIntervalDays`.
|
||||
this.cachedTimestamps.lastFetched = new Date(this.now).toISOString();
|
||||
await this.storage.set(this.cacheFile, this.cachedTimestamps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use previously cached data (local cache only, fast execution) to check
|
||||
* whether this version of the CLI is deprecated as per deprecation policy,
|
||||
* in which case warn about it and conditionally throw an error.
|
||||
*/
|
||||
public async warnAndAbortIfDeprecated() {
|
||||
if (process.env.BALENARC_UNSUPPORTED) {
|
||||
return; // for the benefit of code testing
|
||||
}
|
||||
await this.init();
|
||||
const nextMajorDateStr = this.cachedTimestamps[this.nextMajorVersion];
|
||||
if (!nextMajorDateStr) {
|
||||
return;
|
||||
}
|
||||
const nextMajorDate = new Date(nextMajorDateStr).getTime();
|
||||
const daysElapsed = Math.trunc((this.now - nextMajorDate) / this.msInDay);
|
||||
if (daysElapsed > this.expiryDays) {
|
||||
const { ExpectedError } = await import('./errors');
|
||||
throw new ExpectedError(this.getExpiryMsg(daysElapsed));
|
||||
} else if (daysElapsed > this.deprecationDays && process.stderr.isTTY) {
|
||||
console.error(this.getDeprecationMsg(daysElapsed));
|
||||
}
|
||||
}
|
||||
|
||||
/** Separate function for the benefit of code testing */
|
||||
getDeprecationMsg(daysElapsed: number) {
|
||||
const { warnify } =
|
||||
require('./utils/messages') as typeof import('./utils/messages');
|
||||
return warnify(`\
|
||||
CLI version ${this.nextMajorVersion} was released ${daysElapsed} days ago: please upgrade.
|
||||
This version of the balena CLI (${this.currentVersion}) will exit with an error
|
||||
message after ${this.expiryDays} days from the release of version ${this.nextMajorVersion},
|
||||
as per deprecation policy: https://git.io/JRHUW#deprecation-policy
|
||||
|
||||
The --unsupported flag may be used to bypass this deprecation check and
|
||||
allow the CLI to keep working beyond the deprecation period. However,
|
||||
note that the balenaCloud or openBalena backends may be updated in a way
|
||||
that is no longer compatible with this version.`);
|
||||
}
|
||||
|
||||
/** Separate function the benefit of code testing */
|
||||
getExpiryMsg(daysElapsed: number) {
|
||||
return `
|
||||
This version of the balena CLI (${this.currentVersion}) has expired: please upgrade.
|
||||
${daysElapsed} days have passed since the release of CLI version ${this.nextMajorVersion}.
|
||||
See deprecation policy at: https://git.io/JRHUW#deprecation-policy
|
||||
|
||||
The --unsupported flag may be used to bypass this deprecation check and
|
||||
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.`;
|
||||
}
|
||||
}
|
@ -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.`);
|
||||
@ -174,6 +176,13 @@ const messages: {
|
||||
BalenaExpiredToken: () => stripIndent`
|
||||
Looks like the session token has expired.
|
||||
Try logging in again with the "balena login" command.`,
|
||||
|
||||
BalenaInvalidDeviceType: (error: Error & { deviceTypeSlug?: string }) => {
|
||||
const slug = error.deviceTypeSlug ? `"${error.deviceTypeSlug}"` : 'slug';
|
||||
return stripIndent`
|
||||
Device type ${slug} not recognized. Perhaps misspelled?
|
||||
Check available device types with "balena devices supported"`;
|
||||
},
|
||||
};
|
||||
|
||||
// TODO remove these regexes when we have a way of uniquely indentifying errors.
|
||||
@ -279,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,12 +24,12 @@ 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 <applicationOrDevice>"
|
||||
* That's literally so: "applicationOrDevice" is NOT replaced with the actual
|
||||
* application ID or device ID. The purpose is to find out the most / least
|
||||
* "push <fleetOrDevice>"
|
||||
* That's literally so: "fleetOrDevice" is NOT replaced with the actual
|
||||
* fleet ID or device ID. The purpose is to find out the most / least
|
||||
* used command verbs, so we can focus our development effort where it is most
|
||||
* beneficial to end users.
|
||||
*
|
||||
@ -60,11 +50,10 @@ export async function trackCommand(commandSignature: string) {
|
||||
});
|
||||
}
|
||||
const settings = await import('balena-settings-client');
|
||||
const balenaUrl = settings.get('balenaUrl') as string;
|
||||
|
||||
const username = await (async () => {
|
||||
const getStorage = await import('balena-settings-storage');
|
||||
const dataDirectory = settings.get('dataDirectory') as string;
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
const storage = getStorage({ dataDirectory });
|
||||
let token;
|
||||
try {
|
||||
@ -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));
|
||||
}
|
143
lib/help.ts
143
lib/help.ts
@ -46,7 +46,7 @@ export default class BalenaHelp extends Help {
|
||||
const subject = getHelpSubject(argv);
|
||||
if (!subject) {
|
||||
const verbose = argv.includes('-v') || argv.includes('--verbose');
|
||||
this.showCustomRootHelp(verbose);
|
||||
console.log(this.getCustomRootHelp(verbose));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -80,67 +80,106 @@ export default class BalenaHelp extends Help {
|
||||
throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`);
|
||||
}
|
||||
|
||||
showCustomRootHelp(showAllCommands: boolean): void {
|
||||
const chalk = getChalk();
|
||||
const bold = chalk.bold;
|
||||
const cmd = chalk.cyan.bold;
|
||||
getCustomRootHelp(showAllCommands: boolean): string {
|
||||
const { bold, cyan } = getChalk();
|
||||
|
||||
let commands = this.config.commands;
|
||||
commands = commands.filter((c) => this.opts.all || !c.hidden);
|
||||
|
||||
// Get Primary Commands, sorted as in manual list
|
||||
const primaryCommands = this.manuallySortedPrimaryCommands.map((pc) => {
|
||||
return commands.find((c) => c.id === pc.replace(' ', ':'));
|
||||
});
|
||||
const primaryCommands = this.manuallySortedPrimaryCommands
|
||||
.map((pc) => {
|
||||
return commands.find((c) => c.id === pc.replace(' ', ':'));
|
||||
})
|
||||
.filter((c): c is typeof commands[0] => !!c);
|
||||
|
||||
// Get the rest as Additional Commands
|
||||
const additionalCommands = commands.filter(
|
||||
(c) =>
|
||||
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
|
||||
);
|
||||
|
||||
// Find longest usage, and pad usage of first command in each category
|
||||
// This is to ensure that both categories align visually
|
||||
const usageLength = commands
|
||||
.map((c) => c.usage?.length || 0)
|
||||
.reduce((longest, l) => {
|
||||
return l > longest ? l : longest;
|
||||
});
|
||||
|
||||
if (
|
||||
typeof primaryCommands[0]?.usage === 'string' &&
|
||||
typeof additionalCommands[0]?.usage === 'string'
|
||||
) {
|
||||
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
|
||||
additionalCommands[0].usage =
|
||||
additionalCommands[0].usage.padEnd(usageLength);
|
||||
let usageLength = 0;
|
||||
for (const cmd of primaryCommands) {
|
||||
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
|
||||
}
|
||||
|
||||
// Output help
|
||||
console.log(bold('USAGE'));
|
||||
console.log('$ balena [COMMAND] [OPTIONS]');
|
||||
|
||||
console.log(bold('\nPRIMARY COMMANDS'));
|
||||
console.log(this.formatCommands(primaryCommands));
|
||||
|
||||
let additionalCmdSection: string[];
|
||||
if (showAllCommands) {
|
||||
console.log(bold('\nADDITIONAL COMMANDS'));
|
||||
console.log(this.formatCommands(additionalCommands));
|
||||
// Get the rest as Additional Commands
|
||||
const additionalCommands = commands.filter(
|
||||
(c) =>
|
||||
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
|
||||
);
|
||||
|
||||
// Find longest usage, and pad usage of first command in each category
|
||||
// This is to ensure that both categories align visually
|
||||
for (const cmd of additionalCommands) {
|
||||
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof primaryCommands[0].usage === 'string' &&
|
||||
typeof additionalCommands[0].usage === 'string'
|
||||
) {
|
||||
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
|
||||
additionalCommands[0].usage =
|
||||
additionalCommands[0].usage.padEnd(usageLength);
|
||||
}
|
||||
|
||||
additionalCmdSection = [
|
||||
bold('\nADDITIONAL COMMANDS'),
|
||||
this.formatCommands(additionalCommands),
|
||||
];
|
||||
} else {
|
||||
console.log(
|
||||
`\n${bold('...MORE')} run ${cmd(
|
||||
'balena help --verbose',
|
||||
)} to list additional commands.`,
|
||||
const cmd = cyan.bold('balena help --verbose');
|
||||
additionalCmdSection = [
|
||||
`\n${bold('...MORE')} run ${cmd} to list additional commands.`,
|
||||
];
|
||||
}
|
||||
|
||||
const globalOps = [
|
||||
['--help, -h', 'display command help'],
|
||||
['--debug', 'enable debug output'],
|
||||
[
|
||||
'--unsupported',
|
||||
`\
|
||||
prevent exit with an error as per Deprecation Policy
|
||||
See: https://git.io/JRHUW#deprecation-policy`,
|
||||
],
|
||||
];
|
||||
globalOps[0][0] = globalOps[0][0].padEnd(usageLength);
|
||||
|
||||
const { deprecationPolicyNote, reachingOut } =
|
||||
require('./utils/messages') as typeof import('./utils/messages');
|
||||
|
||||
return [
|
||||
bold('USAGE'),
|
||||
'$ balena [COMMAND] [OPTIONS]',
|
||||
bold('\nPRIMARY COMMANDS'),
|
||||
this.formatCommands(primaryCommands),
|
||||
...additionalCmdSection,
|
||||
bold('\nGLOBAL OPTIONS'),
|
||||
this.formatGlobalOpts(globalOps),
|
||||
bold('\nDeprecation Policy Reminder'),
|
||||
deprecationPolicyNote,
|
||||
reachingOut,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
protected formatGlobalOpts(opts: string[][]) {
|
||||
const { dim } = getChalk();
|
||||
const outLines: string[] = [];
|
||||
let flagWidth = 0;
|
||||
for (const opt of opts) {
|
||||
flagWidth = Math.max(flagWidth, opt[0].length);
|
||||
}
|
||||
for (const opt of opts) {
|
||||
const descriptionLines = opt[1].split('\n');
|
||||
outLines.push(
|
||||
` ${opt[0].padEnd(flagWidth + 2)}${dim(descriptionLines[0])}`,
|
||||
);
|
||||
outLines.push(
|
||||
...descriptionLines
|
||||
.slice(1)
|
||||
.map((line) => ` ${' '.repeat(flagWidth + 2)}${dim(line)}`),
|
||||
);
|
||||
}
|
||||
|
||||
console.log(bold('\nGLOBAL OPTIONS'));
|
||||
console.log(' --help, -h');
|
||||
console.log(' --debug\n');
|
||||
|
||||
const { reachingOut } =
|
||||
require('./utils/messages') as typeof import('./utils/messages');
|
||||
console.log(reachingOut);
|
||||
return outLines.join('\n');
|
||||
}
|
||||
|
||||
protected formatCommands(commands: any[]): string {
|
||||
@ -182,8 +221,8 @@ export default class BalenaHelp extends Help {
|
||||
'push',
|
||||
'logs',
|
||||
'ssh',
|
||||
'apps',
|
||||
'app',
|
||||
'fleets',
|
||||
'fleet',
|
||||
'devices',
|
||||
'device',
|
||||
'tunnel',
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -14,8 +14,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import { exitWithExpectedError } from './errors';
|
||||
|
||||
export let unsupportedFlag = false;
|
||||
|
||||
export interface AppOptions {
|
||||
// Prevent the default behavior of flushing stdout after running a command
|
||||
@ -50,11 +50,16 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
||||
}
|
||||
|
||||
// support global --debug flag
|
||||
const debugIndex = cmdSlice.indexOf('--debug');
|
||||
if (debugIndex > -1) {
|
||||
if (extractBooleanFlag(cmdSlice, '--debug')) {
|
||||
process.env.DEBUG = '1';
|
||||
cmdSlice.splice(debugIndex, 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');
|
||||
}
|
||||
|
||||
// Enable bluebird long stack traces when in debug mode, must be set
|
||||
@ -87,11 +92,22 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
||||
return args;
|
||||
}
|
||||
|
||||
function extractBooleanFlag(argv: string[], flag: string): boolean {
|
||||
const index = argv.indexOf(flag);
|
||||
if (index >= 0) {
|
||||
argv.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the command line refers to a command that has been deprecated
|
||||
* and removed and, if so, exit with an informative error message.
|
||||
*/
|
||||
export function checkDeletedCommand(argvSlice: string[]): void {
|
||||
const { ExpectedError } = require('./errors') as typeof import('./errors');
|
||||
|
||||
if (argvSlice[0] === 'help') {
|
||||
argvSlice = argvSlice.slice(1);
|
||||
}
|
||||
@ -101,17 +117,16 @@ export function checkDeletedCommand(argvSlice: string[]): void {
|
||||
version: string,
|
||||
verb = 'replaced',
|
||||
) {
|
||||
exitWithExpectedError(stripIndent`
|
||||
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
|
||||
Please use "balena ${alternative}" instead.
|
||||
`);
|
||||
throw new ExpectedError(`\
|
||||
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
|
||||
Please use "balena ${alternative}" instead.`);
|
||||
}
|
||||
function removed(oldCmd: string, alternative: string, version: string) {
|
||||
let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`;
|
||||
if (alternative) {
|
||||
msg = [msg, alternative].join('\n');
|
||||
}
|
||||
exitWithExpectedError(msg);
|
||||
throw new ExpectedError(msg);
|
||||
}
|
||||
const stopAlternative =
|
||||
'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.';
|
||||
@ -122,6 +137,13 @@ export function checkDeletedCommand(argvSlice: string[]): void {
|
||||
'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
|
||||
|
@ -64,8 +64,8 @@ export const getDeviceAndAppFromUUID = _.memoize(
|
||||
);
|
||||
if (app == null) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Unable to access the application that device ${deviceUUID} belongs to.
|
||||
Hint: check whether the application owner might have withdrawn access to it.
|
||||
Unable to access the fleet that device ${deviceUUID} belongs to.
|
||||
Hint: check whether the fleet owner withdrew access to it.
|
||||
`);
|
||||
}
|
||||
return [device, app];
|
||||
@ -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,9 +16,9 @@
|
||||
*/
|
||||
import { lowercaseIfSlug } from './normalization';
|
||||
|
||||
export const applicationRequired = {
|
||||
name: 'application',
|
||||
description: 'application name, slug (preferred), or numeric ID (deprecated)',
|
||||
export const fleetRequired = {
|
||||
name: 'fleet',
|
||||
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
|
||||
required: true,
|
||||
parse: lowercaseIfSlug,
|
||||
};
|
||||
|
@ -16,19 +16,16 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
||||
import { stripIndent } from './lazy';
|
||||
import { lowercaseIfSlug } from './normalization';
|
||||
|
||||
export const application = flags.string({
|
||||
char: 'a',
|
||||
description: 'application name, slug (preferred), or numeric ID (deprecated)',
|
||||
parse: lowercaseIfSlug,
|
||||
});
|
||||
// TODO: Consider remove second alias 'app' when we can, to simplify.
|
||||
export const app = flags.string({
|
||||
description: "same as '--application'",
|
||||
import { isV14 } from './version';
|
||||
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
||||
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
|
||||
|
||||
export const fleet = flags.string({
|
||||
char: 'f',
|
||||
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
|
||||
parse: lowercaseIfSlug,
|
||||
});
|
||||
|
||||
@ -73,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`
|
||||
@ -95,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)`,
|
||||
}),
|
||||
};
|
||||
|
17
lib/utils/compose-types.d.ts
vendored
17
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,22 +80,26 @@ 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'
|
||||
| 'composition'
|
||||
| 'source'
|
||||
| 'is_final'
|
||||
| 'contract'
|
||||
| 'semver'
|
||||
| '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>;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user