mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
210 Commits
v12.42.2
...
update-res
Author | SHA1 | Date | |
---|---|---|---|
9b6fb62e4f | |||
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 | |||
0892caa155 | |||
fa4e8e7b55 | |||
e624726e44 | |||
f914fa2d8a | |||
c8f5542c8a | |||
a2cad7bf53 | |||
3a871a0003 | |||
e552e36f7b | |||
c325f1158e | |||
f79ccc0c95 | |||
1ec8d9a4ca | |||
427b0d9b41 | |||
cfd790a193 | |||
36f4c1312b | |||
fe7cbf4f74 | |||
4e8b8fe582 | |||
2986e6cea3 | |||
bb6b4b255a | |||
350c4abb96 | |||
fec96b41ee | |||
1dba5cc7c1 | |||
43c6fe672f | |||
486cae1aaa | |||
4d588e51a7 | |||
0035545ce1 | |||
d559b9a5a1 | |||
e2ffc5f068 | |||
75b2fa0e9e | |||
c619ecd41b | |||
7ed01a925b | |||
460022a7cf | |||
d15b54cf40 | |||
c938df2445 | |||
bf1df05606 | |||
e04242db64 | |||
9265588745 | |||
cd8070b1a6 | |||
b17dad8c60 | |||
a254e46118 | |||
6e7a0defb7 | |||
492dbae7f1 | |||
ccc2c20b6d | |||
501882fd26 | |||
e2ff561728 | |||
5544f4a5dd | |||
a3e90182bc | |||
3ac85dcc5f | |||
1ac573c659 | |||
2c922ee6d2 | |||
d9821939d9 | |||
732fc2d539 | |||
535a443d7c | |||
579d68a8f0 | |||
ffac8cb9e5 | |||
0f2780744f | |||
b4495839ca | |||
f45ac42dd3 | |||
fa26004648 | |||
ba1ea54d69 | |||
9fb62d92b7 | |||
8780a24fb5 | |||
3d3e91d49d | |||
f6e6d9ce8b | |||
0f9d78ab50 | |||
06f7683837 | |||
83a23d9f30 | |||
ffa181a2c3 | |||
d50d18d492 | |||
0b0fb94834 | |||
c1244c0c98 | |||
213e54feb1 | |||
cc8a8513e9 | |||
42c3236313 | |||
91fd515266 | |||
57cd096612 | |||
854501cf8d | |||
d44afa8c39 | |||
b7500fc2c2 | |||
dc6c8d7472 | |||
5c5be8f7b7 | |||
5bdd6c6034 | |||
a5bade99fc | |||
9c3eb76856 | |||
973f1a9c40 | |||
16ea0c9d6d | |||
73bfe545e8 | |||
f53e658ca2 | |||
b66706e8ee | |||
11e50466d5 | |||
431c4b6e4a | |||
d12490f816 | |||
67b7b8b5d0 | |||
16d1f0f06f | |||
9676ea94cb | |||
df8ce0bbe0 | |||
6437bb7511 | |||
ac96616e4e | |||
2737c9c53c | |||
3b8a46f523 | |||
3ac1994941 | |||
b3a6c6cb0f | |||
6d4faa7b2c | |||
9036ce9af3 | |||
4911db640f | |||
e7999f52a9 | |||
68b61e7424 | |||
329b84d01e | |||
25b1dff5d8 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @balena-io/balena-cli
|
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,5 +1,4 @@
|
||||
module.exports = {
|
||||
spec: 'tests/commands/app/create.spec.ts',
|
||||
reporter: 'spec',
|
||||
require: 'ts-node/register/transpile-only',
|
||||
file: './tests/config-tests',
|
||||
|
74
.resinci.yml
74
.resinci.yml
@ -15,77 +15,3 @@ npm:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
|
||||
docker:
|
||||
builds:
|
||||
- path: .
|
||||
dockerfile: ./docker/alpine/Dockerfile
|
||||
docker_repo: balenalib/amd64-alpine-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/amd64-alpine-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/amd64-alpine-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/alpine/Dockerfile
|
||||
docker_repo: balenalib/armv7hf-alpine-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/armv7hf-alpine-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/armv7hf-alpine-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/alpine/Dockerfile
|
||||
docker_repo: balenalib/i386-alpine-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/i386-alpine-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/i386-alpine-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/alpine/Dockerfile
|
||||
docker_repo: balenalib/rpi-alpine-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/rpi-alpine-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/rpi-alpine-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/aarch64-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/aarch64-debian-node:12.19.1-build-20201118
|
||||
- RUN_BASE=balenalib/aarch64-debian-node:12.19.1-run-20201118
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/amd64-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/amd64-debian-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/amd64-debian-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/armv7hf-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/armv7hf-debian-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/armv7hf-debian-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/i386-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/i386-debian-node:12.16.3-build-20200518
|
||||
- RUN_BASE=balenalib/i386-debian-node:12.16.3-build-20200518
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/rpi-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/rpi-debian-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/rpi-debian-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
File diff suppressed because it is too large
Load Diff
509
CHANGELOG.md
509
CHANGELOG.md
@ -4,6 +4,515 @@ 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/).
|
||||
|
||||
## 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]
|
||||
* chore: Remove 'umount' dependency (as advised by "npm audit") [Paulo Castro]
|
||||
* chore: Update oclif [Paulo Castro]
|
||||
* chore: Update dependencies ("npm audit fix") [Paulo Castro]
|
||||
* chore: Update balena-lint [Paulo Castro]
|
||||
|
||||
## 12.44.25 - 2021-07-20
|
||||
|
||||
* Simplify runSpinner api [Pagan Gazzard]
|
||||
|
||||
## 12.44.24 - 2021-07-09
|
||||
|
||||
* Update 'devDependencies' in package.json [Paulo Castro]
|
||||
* config write: Fix parsing of 'key' argument with numeric components [Paulo Castro]
|
||||
|
||||
## 12.44.23 - 2021-06-30
|
||||
|
||||
* Delete CODEOWNERS [dfunckt]
|
||||
|
||||
## 12.44.22 - 2021-06-24
|
||||
|
||||
* preload: Warn that zip files are only accepted for Intel Edison [Kyle Harding]
|
||||
* preload: Catch sfdisk errors that result in an endless spinner [Kyle Harding]
|
||||
|
||||
## 12.44.21 - 2021-06-22
|
||||
|
||||
* Update author details in package.json, Windows Programs and Features [Paulo Castro]
|
||||
|
||||
## 12.44.20 - 2021-06-14
|
||||
|
||||
* devices supported: Use new DeviceType data model as source of truth [Paulo Castro]
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk from 15.31.0 to 15.36.0 [Paulo Castro] </summary>
|
||||
|
||||
> ### balena-sdk-15.36.0 - 2021-05-13
|
||||
>
|
||||
> * Add is_of__class in application model [JSReds]
|
||||
>
|
||||
> ### balena-sdk-15.35.2 - 2021-05-13
|
||||
>
|
||||
> * Fix lint after prittier update [JSReds]
|
||||
>
|
||||
> ### balena-sdk-15.35.1 - 2021-05-06
|
||||
>
|
||||
> * Add missing types for the Service & Image resources [Thodoris Greasidis]
|
||||
> * release.getLatestByApplication: Fix the return type to be optional [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.35.0 - 2021-05-05
|
||||
>
|
||||
> * Deprecate application.getAllWithDeviceServiceDetails() [Thodoris Greasidis]
|
||||
> * Deprecate targeting/retrieving apps by name in all models [Thodoris Greasidis]
|
||||
> * Add application.getAppByName method [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.34.2 - 2021-05-05
|
||||
>
|
||||
> * Abstract & update device os info that are used in tests [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.34.1 - 2021-04-14
|
||||
>
|
||||
> * types: Deprecate Device is_in_local_mode property [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.34.0 - 2021-04-13
|
||||
>
|
||||
> * Deprecate user__is_member_of__application in favor of the term form [Thodoris Greasidis]
|
||||
> * types/models: Add `user_application_membership` to the User & Team [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.33.0 - 2021-04-12
|
||||
>
|
||||
> * types/modes: Deprecate Subscription's discounts__plan_addon property [Thodoris Greasidis]
|
||||
> * types/models: Add `subscription_addon_discount` to the Subscription [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.32.0 - 2021-04-08
|
||||
>
|
||||
> * Add the auth.getUserActorId() method [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.31.2 - 2021-04-08
|
||||
>
|
||||
> * models/application: Fix examples incorrectly showing a short device uuid [Thodoris Greasidis]
|
||||
> * Add application.get example using the application slug [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.31.1 - 2021-04-07
|
||||
>
|
||||
> * Refactor the resource get by field tests [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.44.19 - 2021-05-28
|
||||
|
||||
* Fix fast-boot module caching with read-only installation folders [Paulo Castro]
|
||||
|
||||
## 12.44.18 - 2021-05-27
|
||||
|
||||
* Update advanced installation instructions [Paulo Castro]
|
||||
|
||||
## 12.44.17 - 2021-05-24
|
||||
|
||||
* doc: Document the image nameing scheme used by deploy [Robert Günzler]
|
||||
|
||||
## 12.44.16 - 2021-05-18
|
||||
|
||||
* preload: Avoid hardcoded registry2 URLs with openBalena [Kyle Harding]
|
||||
|
||||
## 12.44.15 - 2021-05-16
|
||||
|
||||
* os configure, local configure: Fix "Unsupported filesystem" error [Roland Kajatin]
|
||||
|
||||
## 12.44.14 - 2021-05-09
|
||||
|
||||
* Add completion generator from oclif.manifest.json [Pierre Kancir]
|
||||
|
||||
## 12.44.13 - 2021-05-07
|
||||
|
||||
* Update dependencies (multibuild, dockerode, docker-toolbelt, docker-progress) [Paulo Castro]
|
||||
|
||||
<details>
|
||||
<summary> preload: Improve error handling (stop spinning wheels) [Paulo Castro] </summary>
|
||||
|
||||
> ### balena-preload-10.4.6 - 2021-05-06
|
||||
>
|
||||
> * Update dependencies (dockerode, docker-progress) [Paulo Castro]
|
||||
> * Don't assume that 'docker' argument uses Bluebird promises [Paulo Castro]
|
||||
>
|
||||
> ### balena-preload-10.4.5 - 2021-05-05
|
||||
>
|
||||
> * Enhance comms between CLI process and Python process in container [Paulo Castro]
|
||||
>
|
||||
> ### balena-preload-10.4.4 - 2021-05-05
|
||||
>
|
||||
> * Fix unhandled exception on container.wait() [Paulo Castro]
|
||||
> * Emit error on container status code '137' too (OOM SIGKILL) [Paulo Castro]
|
||||
> * Add flake8 npm script [Paulo Castro]
|
||||
>
|
||||
> ### balena-preload-10.4.3 - 2021-05-04
|
||||
>
|
||||
> * Don't truncate error logs (add _truncate_exc option to python sh commands) [Paulo Castro]
|
||||
> * Add keyword args to RetryCounter key computation. Remove unused args. [Paulo Castro]
|
||||
>
|
||||
</details>
|
||||
|
||||
* build: Workaround issue with Docker v20.10 + single-arch base images [Paulo Castro]
|
||||
* build: Fix "Total: undefined" image pull progress report [Paulo Castro]
|
||||
|
||||
## 12.44.12 - 2021-04-27
|
||||
|
||||
* preload: Update to Docker 20.10 with cgroups v2 support [Kyle Harding]
|
||||
|
||||
## 12.44.11 - 2021-04-21
|
||||
|
||||
* Add message regarding deprecation of --buildArg option in build/deploy commands [Scott Lowe]
|
||||
|
||||
## 12.44.10 - 2021-04-15
|
||||
|
||||
* ssh: fix incorrect service name parsing in local mode [Tomás Migone]
|
||||
|
||||
## 12.44.9 - 2021-04-13
|
||||
|
||||
* config inject/read/write: Fix umount errors with OS image files [Paulo Castro]
|
||||
* Refactor dependency import in utils/helpers.ts for performance [Paulo Castro]
|
||||
|
||||
## 12.44.8 - 2021-04-09
|
||||
|
||||
* push, logs: Fix parsing of local mode device logs (NDJSON stream) [Paulo Castro]
|
||||
|
||||
## 12.44.7 - 2021-04-09
|
||||
|
||||
* lib/commands/local/configure: Fix local configure when resin-wifi is not available on the image [Marios Balamatsias]
|
||||
|
||||
## 12.44.6 - 2021-04-07
|
||||
|
||||
* Direct missing release installs to npm install method [Miguel Casqueira]
|
||||
|
||||
## 12.44.5 - 2021-04-07
|
||||
|
||||
* docs: Update install instructions re macOS installer notarization [Paulo Castro]
|
||||
* Update resin-compose-parse from 2.1.2 to 2.1.3 [Paulo Castro]
|
||||
* Update balena-config-json from 4.1.0 to 4.1.1 [Paulo Castro]
|
||||
* Update etcher-sdk from 6.2.0 to 6.2.1 [Paulo Castro]
|
||||
* Update balena-sdk from 15.29.0 to 15.31.0 [Paulo Castro]
|
||||
|
||||
## 12.44.4 - 2021-04-06
|
||||
|
||||
* Add notarization for macOS graphical installer [Dan Goodman]
|
||||
|
||||
## 12.44.3 - 2021-04-04
|
||||
|
||||
* docs: Further clarify Docker requirements for preload [Paulo Castro]
|
||||
|
||||
## 12.44.2 - 2021-04-02
|
||||
|
||||
* docker: Remove references to CLI docker images in the installation docs [Paulo Castro]
|
||||
* docker: Remove balenalib images and docs [Kyle Harding]
|
||||
|
||||
## 12.44.1 - 2021-03-31
|
||||
|
||||
* os/configure: Fix broken NetworkManager URL [Mark Corbin]
|
||||
|
||||
## 12.44.0 - 2021-03-30
|
||||
|
||||
* osConfigure/localFlash: Add support for Node.js v14 [Marios Balamatsias]
|
||||
|
||||
## 12.43.2 - 2021-03-26
|
||||
|
||||
* docker: Improve handling of Docker-in-Docker errors [Kyle Harding]
|
||||
|
||||
## 12.43.1 - 2021-03-25
|
||||
|
||||
* Improve installation docs regarding Docker Desktop version requirements [Paulo Castro]
|
||||
|
||||
## 12.43.0 - 2021-03-23
|
||||
|
||||
* Refactor automation scripts (reduce need for MSYS to build on Windows) [Paulo Castro]
|
||||
* Add macOS uninstall script (sudo /usr/local/lib/balena-cli/bin/uninstall) [Paulo Castro]
|
||||
* Bump `patch-package` dependency and remove its own patch file [Paulo Castro]
|
||||
|
||||
## 12.42.2 - 2021-03-20
|
||||
|
||||
* push: Fix docker-compose.dev.yml serialization ("should be object,null" error) [Paulo Castro]
|
||||
|
@ -127,11 +127,15 @@ The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
|
||||
## Windows
|
||||
|
||||
The `npm run build:installer` script (which generates the `.exe` executable installer on Windows)
|
||||
specifically requires [MSYS2](https://www.msys2.org/) to be installed. Other than that, the
|
||||
standard Command Prompt or PowerShell can be used (though MSYS2 is still handy, as it provides
|
||||
'git' and a number of common unix utilities). If changes are made to npm scripts in `package.json`,
|
||||
check that they also run on a standard Windows Command Prompt.
|
||||
Besides the regular npm installation dependencies, the `npm run build:installer` script
|
||||
that produces the `.exe` graphical installer on Windows also requires
|
||||
[NSIS](https://sourceforge.net/projects/nsis/) and [MSYS2](https://www.msys2.org/) to be
|
||||
installed. Be sure to add `C:\Program Files (x86)\NSIS` to the PATH, so that `makensis`
|
||||
is available. MSYS2 is recommended when developing the balena CLI on Windows.
|
||||
|
||||
If changes are made to npm scripts in `package.json`, don't assume that a Unix shell like
|
||||
bash is available. For example, some Windows shells don't have the `cp` and `rm` commands,
|
||||
which is why you'll often find `ncp` and `rimraf` used in `package.json` scripts.
|
||||
|
||||
## Updating the 'npm-shrinkwrap.json' file
|
||||
|
||||
@ -164,6 +168,24 @@ Optionally, these steps may be automated by installing the
|
||||
npx npm-merge-driver install -g
|
||||
```
|
||||
|
||||
## `fast-boot` and `npm link` - modifying the `node_modules` folder
|
||||
|
||||
During development or debugging, it is sometimes useful to temporarily modify the `node_modules`
|
||||
folder (with or without making the respective changes to the `npm-shrinkwrap.json` file),
|
||||
replacing dependencies with different versions. This can be achieved with the `npm link`
|
||||
command, or by manually editing or copying files to the `node_modules` folder.
|
||||
|
||||
Unexpected behavior may then be observed because of the CLI's use of the
|
||||
[fast-boot2](https://www.npmjs.com/package/fast-boot2) package that caches module resolution.
|
||||
`fast-boot2` is configured in `lib/fast-boot.ts` to automatically invalidate the cache if
|
||||
changes are made to the `package.json` or `npm-shrinkwrap.json` files, but the cache won't
|
||||
be automatically invalidated if `npm link` is used or if manual modifications are made to the
|
||||
`node_modules` folder. In this situation:
|
||||
|
||||
* Manually delete the module cache file (typically `~/.balena/cli-module-cache.json`), or
|
||||
* Use the `bin/balena-dev` entry point (instead of `bin/balena`) as it does not activate
|
||||
`fast-boot2`.
|
||||
|
||||
## TypeScript and oclif
|
||||
|
||||
The CLI currently contains a mix of plain JavaScript and
|
||||
@ -237,3 +259,12 @@ 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'],`
|
||||
|
||||
|
@ -13,8 +13,6 @@ There are 3 options to choose from to install balena's CLI:
|
||||
Recommended also for scripted installation in CI (continuous integration) environments.
|
||||
* [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested
|
||||
in integrating the balena CLI in their existing projects or workflow.
|
||||
* [Docker Installation](#docker-installation): recommended for users that would like to run the
|
||||
CLI on edge devices or systems where npm installation may not be an option.
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
@ -62,7 +60,7 @@ macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm)
|
||||
|
||||
> * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/1479).
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/2244).
|
||||
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with
|
||||
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
|
||||
> For these, consider the [NPM Installation](#npm-installation) option.
|
||||
@ -78,56 +76,76 @@ as described above.
|
||||
|
||||
If you are a Node.js developer, you may wish to install the balena CLI via [npm](https://www.npmjs.com).
|
||||
The npm installation involves building native (platform-specific) binary modules, which require
|
||||
some additional development tools to be installed first:
|
||||
some development tools to be installed first, as follows.
|
||||
|
||||
* [Node.js](https://nodejs.org/) version 10 (min **10.20.0**) or 12 (version 14 is not yet fully supported)
|
||||
* **Linux, macOS** and **Windows Subsystem for Linux (WSL):**
|
||||
Installing Node via [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) is recommended.
|
||||
When the "system" or "default" Node.js and npm packages are installed with "apt-get" in Linux
|
||||
distributions like Ubuntu, users often report permission or compilation errors when running
|
||||
"npm install".
|
||||
* [Python 2.7](https://www.python.org/), [git](https://git-scm.com/), [make](https://www.gnu.org/software/make/), [g++](https://gcc.gnu.org/)
|
||||
* **Linux** and **Windows Subsystem for Linux (WSL):**
|
||||
`sudo apt-get install -y python git make g++`
|
||||
* **macOS:** install Apple's Command Line Tools by running on a Terminal window:
|
||||
`xcode-select --install`
|
||||
> **The balena CLI currently requires Node.js version 10 (min 10.20.0) or 12.**
|
||||
> **Versions 13 and later are not yet fully supported.**
|
||||
|
||||
On **Windows (not WSL),** the dependencies above and additional ones can be met by installing:
|
||||
### Install development tools
|
||||
|
||||
* Node.js from the [Nodejs.org download page](https://nodejs.org/en/download/).
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++`, `ssh`, `rsync`
|
||||
and more:
|
||||
* `pacman -S git openssh rsync gcc make`
|
||||
#### **Linux or WSL** (Windows Subsystem for Linux)
|
||||
|
||||
```sh
|
||||
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 12
|
||||
```
|
||||
|
||||
The `curl` command line above uses
|
||||
[nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script) to install
|
||||
Node.js, instead of using `apt-get`. Installing Node.js through `apt-get` is a common source of
|
||||
problems from permission errors to conflict with other system packages, and therefore not
|
||||
recommended.
|
||||
|
||||
#### **macOS**
|
||||
|
||||
* Download and install Apple's Command Line Tools from https://developer.apple.com/downloads/
|
||||
* Install Node.js through [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script):
|
||||
|
||||
```sh
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 12
|
||||
```
|
||||
|
||||
#### **Windows** (not WSL)
|
||||
|
||||
Install:
|
||||
|
||||
* Node.js v12 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||
* If you'd like the ability to switch between Node.js versions, install
|
||||
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||
instead.
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||
* `pacman -S git gcc make openssh p7zip`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based
|
||||
interactive CLI menus to misbehave. [Check this Github issue for a
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
* The Windows Driver Kit (WDK), which is needed to compile some native Node modules. It is **not**
|
||||
necessary to install Visual Studio, only the WDK, which is "step 2" in the following guides:
|
||||
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-wdk-for-windows-10-version-1903)
|
||||
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-refreshed-wdk-for-windows-10-version-2004)
|
||||
* [WDK for earlier versions of Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads#step-2-install-the-wdk)
|
||||
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package (which
|
||||
provides Python 2.7 and more), by running the following command on an [administrator
|
||||
console](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/):
|
||||
|
||||
`npm install -g --production windows-build-tools`
|
||||
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package,
|
||||
by running the following command on an [administrator
|
||||
console](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/):
|
||||
`npm install --global --production windows-build-tools`
|
||||
|
||||
With these dependencies in place, the balena CLI installation command is:
|
||||
### Install the balena CLI
|
||||
|
||||
After installing the development tools, install the balena CLI with:
|
||||
|
||||
```sh
|
||||
$ npm install balena-cli -g --production --unsafe-perm
|
||||
$ npm install balena-cli --global --production --unsafe-perm
|
||||
```
|
||||
|
||||
`--unsafe-perm` is required when `npm install` is executed as the root user, or on systems where
|
||||
the global install directory is not user-writable. It allows npm install steps to download and save
|
||||
prebuilt native binaries, and also allows the execution of npm scripts like `postinstall` that are
|
||||
used to patch dependencies. It is usually possible to omit `--unsafe-perm` if installing under a
|
||||
regular (non-root) user account, especially if using a user-managed node installation such as
|
||||
[nvm](https://github.com/creationix/nvm).
|
||||
`--unsafe-perm` is needed when `npm install` is executed as the `root` user (e.g. in a Docker
|
||||
container) in order to allow npm scripts like `postinstall` to be executed.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
The `balena ssh`, `scan`, `build`, `deploy`, `preload` and `os configure` commands may require
|
||||
The `balena ssh`, `scan`, `build`, `deploy` and `preload` commands may require
|
||||
additional software to be installed. Check the Additional Dependencies sections for each operating
|
||||
system:
|
||||
|
||||
@ -135,9 +153,9 @@ system:
|
||||
* [macOS](./INSTALL-MAC.md#additional-dependencies)
|
||||
* [Linux](./INSTALL-LINUX.md#additional-dependencies)
|
||||
|
||||
The `build` and `deploy` commands are also capable of using Docker or balenaEngine on a remote
|
||||
server, or on a balenaOS device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images)). Reasons why this
|
||||
Where Docker or balenaEngine are required, they may be installed on the local machine (where the
|
||||
balena CLI is executed), on a remote server, or on a balenaOS device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images). Reasons why this
|
||||
may be desirable include:
|
||||
|
||||
* To avoid having to install Docker on the development machine / laptop.
|
||||
@ -145,13 +163,7 @@ may be desirable include:
|
||||
* To build or run images "natively" on an ARM device, avoiding the need for QEMU emulation.
|
||||
|
||||
To use a remote Docker Engine (daemon) or balenaEngine, specify the remote machine's IP address and
|
||||
port number with the `--dockerHost` and `--dockerPort` command-line options. For more details,
|
||||
check `balena help build` or the [online
|
||||
port number with the `--dockerHost` and `--dockerPort` command-line options. The `preload` command
|
||||
has additional requirements because the bind mount feature is used. For more details, see
|
||||
`balena help` for each command or the [online
|
||||
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).
|
||||
|
||||
## Docker Installation
|
||||
|
||||
[balenalib images](https://www.balena.io/docs/reference/base-images/base-images/)
|
||||
are available for the balena CLI. They can be used interactively with `docker run`, or
|
||||
as a base image for your application containers. Check out [Docker.md](docker/DOCKER.md)
|
||||
on how to pick an image and get started!
|
||||
|
@ -1,8 +1,10 @@
|
||||
# balena CLI Installation Instructions for Linux
|
||||
|
||||
These instructions are for the recommended installation option. They are suitable for most Linux
|
||||
distributions, except notably for **Linux Alpine** or **Busybox**. For these distros, see [advanced
|
||||
installation options](./INSTALL-ADVANCED.md).
|
||||
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**
|
||||
|
||||
@ -53,11 +55,11 @@ steps](https://docs.docker.com/engine/install/linux-postinstall/) on how to achi
|
||||
### build, deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote
|
||||
machine). Most users will follow [Docker's installation
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation (laptop) as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
|
@ -10,30 +10,36 @@ 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 below.
|
||||
and `preload` commands may require additional software to be installed, as described
|
||||
in the next section.
|
||||
|
||||
To update the balena CLI, repeat the steps above for the new version.
|
||||
To uninstall it, run the following command on a terminal prompt:
|
||||
|
||||
```text
|
||||
sudo /usr/local/lib/balena-cli/bin/uninstall
|
||||
```
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
### build and deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote
|
||||
machine). Most users will follow [Docker's installation
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation (laptop) as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
@ -52,17 +58,17 @@ command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena preload
|
||||
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker, with the additional
|
||||
restriction that Docker must be installed on the local machine (because Docker's bind mounting
|
||||
feature is used). Also, for some device types (such as the Raspberry Pi), the `preload` command
|
||||
requires Docker to support the [AUFS storage
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker.
|
||||
Preloading balenaOS images for some older device types (like the Raspberry
|
||||
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
|
||||
for Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1. The
|
||||
present workaround is to either:
|
||||
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
|
||||
18.06.1. The present workarounds are to either:
|
||||
|
||||
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
|
||||
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
|
||||
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
|
||||
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
|
||||
Mac](https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18061-ce-mac73-2018-08-29)
|
||||
* Install the balena CLI on a Linux machine (as Docker for Linux still supports AUFS). A Linux
|
||||
Virtual Machine also works, but a Docker container is _not_ recommended.
|
||||
|
||||
Long term, we are working on replacing AUFS with overlay2 for the affected device types.
|
||||
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.
|
||||
|
@ -10,19 +10,17 @@ 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`, `preload` and `os configure` commands may require additional software to be installed, as
|
||||
`deploy` and `preload` commands may require additional software to be installed, as
|
||||
described below.
|
||||
|
||||
## Additional Dependencies
|
||||
@ -30,11 +28,11 @@ described below.
|
||||
### build and deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote
|
||||
machine). Most users will follow [Docker's installation
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation (laptop) as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
@ -59,24 +57,17 @@ Otherwise, Bonjour for Windows can be downloaded and installed from: https://sup
|
||||
|
||||
### balena preload
|
||||
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker, with the additional
|
||||
restriction that Docker must be installed on the local machine (because Docker's bind mounting
|
||||
feature is used). Also, for some device types (such as the Raspberry Pi), the `preload` command
|
||||
requires Docker to support the [AUFS storage
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker.
|
||||
Preloading balenaOS images for some older device types (like the Raspberry
|
||||
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
|
||||
for Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1. The
|
||||
present workaround is to either:
|
||||
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
|
||||
18.06.1. The present workarounds are to either:
|
||||
|
||||
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
|
||||
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
|
||||
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
|
||||
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
|
||||
Windows](https://docs.docker.com/docker-for-windows/release-notes/#docker-community-edition-18061-ce-win73-2018-08-29)
|
||||
* Install the balena CLI on a Linux machine (as Docker for Linux still supports AUFS). A Linux
|
||||
Virtual Machine also works, but a Docker container is _not_ recommended.
|
||||
|
||||
Long term, we are working on replacing AUFS with overlay2 for the affected device types.
|
||||
|
||||
### balena os configure
|
||||
|
||||
* The `balena os configure` command is currently not supported on Windows natively, but works with
|
||||
the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL). When
|
||||
using WSL, [install the balena CLI for
|
||||
Linux](https://github.com/balena-io/balena-cli/blob/master/INSTALL-LINUX.md).
|
||||
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.
|
||||
|
26
README.md
26
README.md
@ -28,13 +28,13 @@ are supported. Alternative shells include:
|
||||
|
||||
* [MSYS2](https://www.msys2.org/):
|
||||
* Install additional packages with the command:
|
||||
`pacman -S git openssh rsync`
|
||||
`pacman -S git gcc make openssh p7zip`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based interactive CLI
|
||||
menus to break. [Check this Github issue for a
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS): select the `msys-rsync` and `msys-openssh` packages too
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS)
|
||||
* [Git for Windows](https://git-for-windows.github.io/)
|
||||
* During the installation, you will be prompted to choose between _"Use MinTTY"_ and _"Use
|
||||
Windows' default console window"._ Choose the latter, because of the same [MSYS2
|
||||
@ -50,14 +50,14 @@ are supported. Alternative shells include:
|
||||
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. Optionally, `bash` command
|
||||
auto completion may be enabled by copying the
|
||||
[balena-completion.bash](https://github.com/balena-io/balena-cli/blob/master/balena-completion.bash)
|
||||
[balena_comp](https://github.com/balena-io/balena-cli/blob/master/completion/balena-completion.bash)
|
||||
file to your system's `bash_completion` directory: check [Docker's command completion
|
||||
guide](https://docs.docker.com/compose/completion/) for system setup instructions.
|
||||
|
||||
## 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
|
||||
@ -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:
|
||||
|
@ -22,27 +22,31 @@ 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 { promisify } from 'util';
|
||||
|
||||
import { stripIndent } from '../lib/utils/lazy';
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import {
|
||||
diffLines,
|
||||
getSubprocessStdout,
|
||||
loadPackageJson,
|
||||
MSYS2_BASH,
|
||||
ROOT,
|
||||
StdOutTap,
|
||||
whichSpawn,
|
||||
} from './utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
const MSYS2_BASH =
|
||||
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
|
||||
function dPath(...paths: string[]) {
|
||||
return path.join(ROOT, 'dist', ...paths);
|
||||
@ -243,7 +247,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 {
|
||||
@ -260,6 +274,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)');
|
||||
}
|
||||
|
||||
@ -296,6 +314,88 @@ async function zipPkg() {
|
||||
});
|
||||
}
|
||||
|
||||
async function signFilesForNotarization() {
|
||||
console.log('Deleting unneeded zip files...');
|
||||
await new Promise((resolve, reject) => {
|
||||
klaw('node_modules/')
|
||||
.on('data', (item: { path: string; stats: Stats }) => {
|
||||
if (!item.stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
path.basename(item.path).endsWith('.zip') &&
|
||||
path.dirname(item.path).includes('test')
|
||||
) {
|
||||
console.log('Removing zip', item.path);
|
||||
fs.unlinkSync(item.path);
|
||||
}
|
||||
})
|
||||
.on('end', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
// Sign all .node files first
|
||||
console.log('Signing .node files...');
|
||||
await new Promise((resolve, reject) => {
|
||||
klaw('node_modules/')
|
||||
.on('data', async (item: { path: string; stats: Stats }) => {
|
||||
if (!item.stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (path.basename(item.path).endsWith('.node')) {
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
item.path,
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
item.path,
|
||||
]);
|
||||
}
|
||||
})
|
||||
.on('end', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
console.log('Signing other binaries...');
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/denymount/bin/denymount',
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/denymount/bin/denymount',
|
||||
]);
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/macmount/bin/macmount',
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/macmount/bin/macmount',
|
||||
]);
|
||||
}
|
||||
|
||||
export async function buildStandaloneZip() {
|
||||
console.log(`Building standalone zip package for CLI ${version}`);
|
||||
try {
|
||||
@ -326,8 +426,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',
|
||||
@ -343,6 +441,20 @@ async function signWindowsInstaller() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Apple Installer Notarization to continue
|
||||
*/
|
||||
async function notarizeMacInstaller(): Promise<void> {
|
||||
const appleId = 'accounts+apple@balena.io';
|
||||
const { notarize } = await import('electron-notarize');
|
||||
await notarize({
|
||||
appBundleId: 'io.balena.etcher',
|
||||
appPath: renamedOclifInstallers.darwin,
|
||||
appleId,
|
||||
appleIdPassword: '@keychain:CLI_PASSWORD',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
|
||||
* of process.platform) to generate the native installers (which end up under
|
||||
@ -369,6 +481,10 @@ export async function buildOclifInstaller() {
|
||||
console.log(`rimraf(${dir})`);
|
||||
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
console.log('Signing files for notarization...');
|
||||
await signFilesForNotarization();
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
@ -381,6 +497,10 @@ export async function buildOclifInstaller() {
|
||||
// (`oclif.macos.sign` section).
|
||||
if (process.platform === 'win32') {
|
||||
await signWindowsInstaller();
|
||||
} else if (process.platform === 'darwin') {
|
||||
console.log('Notarizing package...');
|
||||
await notarizeMacInstaller(); // Notarize
|
||||
console.log('Package notarized.');
|
||||
}
|
||||
console.log(`oclif installer build completed`);
|
||||
}
|
||||
|
@ -34,15 +34,22 @@ const capitanoDoc = {
|
||||
files: ['build/commands/api-key/generate.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
title: 'Fleet',
|
||||
files: [
|
||||
'build/commands/apps.js',
|
||||
'build/commands/fleets.js',
|
||||
'build/commands/app/index.js',
|
||||
'build/commands/fleet/index.js',
|
||||
'build/commands/app/create.js',
|
||||
'build/commands/fleet/create.js',
|
||||
'build/commands/app/purge.js',
|
||||
'build/commands/fleet/purge.js',
|
||||
'build/commands/app/rename.js',
|
||||
'build/commands/fleet/rename.js',
|
||||
'build/commands/app/restart.js',
|
||||
'build/commands/fleet/restart.js',
|
||||
'build/commands/app/rm.js',
|
||||
'build/commands/fleet/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -75,6 +82,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',
|
||||
];
|
||||
|
||||
@ -86,7 +86,7 @@ function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
|
||||
const command: OclifCommand =
|
||||
jsFilename === 'help'
|
||||
? ((new FakeHelpCommand() as unknown) as OclifCommand)
|
||||
? (new FakeHelpCommand() as unknown as OclifCommand)
|
||||
: (require(path.join(process.cwd(), jsFilename)).default as OclifCommand);
|
||||
|
||||
return [command];
|
||||
@ -101,7 +101,7 @@ async function printMarkdown() {
|
||||
console.log(await renderMarkdown());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,17 +41,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,17 +54,18 @@ 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a cached Octokit instance, creating a new one as needed. */
|
||||
const getOctokit = _.once(function () {
|
||||
const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin(
|
||||
(require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling'))
|
||||
.throttling,
|
||||
const Octokit = (
|
||||
require('@octokit/rest') as typeof import('@octokit/rest')
|
||||
).Octokit.plugin(
|
||||
(
|
||||
require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling')
|
||||
).throttling,
|
||||
);
|
||||
return new Octokit({
|
||||
auth: GITHUB_TOKEN,
|
||||
@ -110,7 +111,8 @@ function getPageNumbers(
|
||||
if (!response.headers.link) {
|
||||
return res;
|
||||
}
|
||||
const parse = require('parse-link-header') as typeof import('parse-link-header');
|
||||
const parse =
|
||||
require('parse-link-header') as typeof import('parse-link-header');
|
||||
const parsed = parse(response.headers.link);
|
||||
if (parsed == null) {
|
||||
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
|
||||
@ -158,11 +160,14 @@ async function updateGitHubReleaseDescriptions(
|
||||
per_page: perPage,
|
||||
});
|
||||
let errCount = 0;
|
||||
for await (const response of octokit.paginate.iterator(options)) {
|
||||
const { page: thisPage, pages: totalPages, ordinal } = getPageNumbers(
|
||||
response,
|
||||
perPage,
|
||||
);
|
||||
type Release =
|
||||
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
|
||||
for await (const response of octokit.paginate.iterator<Release>(options)) {
|
||||
const {
|
||||
page: thisPage,
|
||||
pages: totalPages,
|
||||
ordinal,
|
||||
} = getPageNumbers(response, perPage);
|
||||
let i = 0;
|
||||
for (const cliRelease of response.data) {
|
||||
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
|
||||
|
@ -27,7 +27,6 @@ import {
|
||||
release,
|
||||
updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
} from './deploy-bin';
|
||||
import { fixPathForMsys, ROOT, runUnderMsys } from './utils';
|
||||
|
||||
// DEBUG set to falsy for negative values else is truthy
|
||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
@ -36,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:
|
||||
@ -48,17 +42,14 @@ function exitWithError(error: Error | string): never {
|
||||
* 'build:standalone' (to build a standalone pkg package)
|
||||
* 'release' (to create/update a GitHub release)
|
||||
*
|
||||
* In the case of 'build:installer', also call runUnderMsys() to switch the
|
||||
* shell from cmd.exe to MSYS2 bash.exe.
|
||||
*
|
||||
* @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,
|
||||
@ -70,14 +61,10 @@ 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If runUnderMsys() is called to re-execute this script under MSYS2,
|
||||
// the current working dir becomes the MSYS2 homedir, so we change back.
|
||||
process.chdir(ROOT);
|
||||
|
||||
// The BUILD_TMP env var is used as an alternative location for oclif
|
||||
// (patched) to copy/extract the CLI files, run npm install and then
|
||||
// create the NSIS executable installer for Windows. This was necessary
|
||||
@ -95,29 +82,25 @@ export async function run(args?: string[]) {
|
||||
|
||||
for (const arg of args) {
|
||||
try {
|
||||
if (arg === 'build:installer' && process.platform === 'win32') {
|
||||
// ensure running under MSYS2
|
||||
if (!process.env.MSYSTEM) {
|
||||
process.env.MSYS2_PATH_TYPE = 'inherit';
|
||||
await runUnderMsys([
|
||||
fixPathForMsys(process.argv[0]),
|
||||
fixPathForMsys(process.argv[1]),
|
||||
arg,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
if (process.env.MSYS2_PATH_TYPE !== 'inherit') {
|
||||
throw new Error(
|
||||
'the MSYS2_PATH_TYPE env var must be set to "inherit"',
|
||||
);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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}'`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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,13 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await $main();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -18,10 +18,7 @@
|
||||
import { spawn } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as shellEscape from 'shell-escape';
|
||||
|
||||
export const MSYS2_BASH =
|
||||
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
/** Tap and buffer this process' stdout and stderr */
|
||||
@ -91,93 +88,6 @@ export function loadPackageJson() {
|
||||
return require(path.join(ROOT, 'package.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
|
||||
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
|
||||
*/
|
||||
export function fixPathForMsys(p: string): string {
|
||||
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
|
||||
* The given argv arguments are escaped using the 'shell-escape' package,
|
||||
* so that backslashes in Windows paths, and other bash-special characters,
|
||||
* are preserved. If argv is not provided, defaults to process.argv, to the
|
||||
* effect that this current (parent) process is re-executed under MSYS2 bash.
|
||||
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
|
||||
* Windows.
|
||||
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
|
||||
*/
|
||||
export async function runUnderMsys(argv?: string[]) {
|
||||
const newArgv = argv || process.argv;
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = ['-lc', shellEscape(newArgv)];
|
||||
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
|
||||
child.on('close', (code) => {
|
||||
if (code) {
|
||||
console.log(`runUnderMsys: child process exited with code ${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]'),
|
||||
);
|
||||
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
|
||||
|
@ -1,73 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
commands="app apps build config deploy device devices env envs help key \
|
||||
keys local login logout logs note os preload quickstart settings \
|
||||
scan ssh util version whoami"
|
||||
# Sub-completions
|
||||
app_cmds="create restart rm"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="identify init move public-url reboot register rename rm \
|
||||
shutdown"
|
||||
device_public_url_cmds="disable enable status"
|
||||
env_cmds="add rename rm"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
util_cmds="available-drives"
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"app")
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
"config")
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
"device")
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
"env")
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
"key")
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
"local")
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
"os")
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
"util")
|
||||
COMPREPLY=( $(compgen -W "$util_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
elif [ $COMP_CWORD -eq 3 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"public-url")
|
||||
COMPREPLY=( $(compgen -W "$device_public_url_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
19
bin/balena
19
bin/balena
@ -9,14 +9,15 @@ process.env.UV_THREADPOOL_SIZE = '64';
|
||||
// Disable oclif registering ts-node
|
||||
process.env.OCLIF_TS_NODE = 0;
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheScope: __dirname + '/..',
|
||||
cacheFile: __dirname + '/.fast-boot.json',
|
||||
});
|
||||
async function run() {
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
await require('../build/fast-boot').start();
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
// Run the CLI
|
||||
require('../build/app').run();
|
||||
// Run the CLI
|
||||
await require('../build/app').run();
|
||||
}
|
||||
|
||||
run();
|
||||
|
@ -11,6 +11,22 @@
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
|
||||
// * fast-boot2's cacheKiller option is configured to include the timestamps of
|
||||
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
|
||||
// behavior when changes are made to dependencies during development. This is
|
||||
// generally a good thing, however, `balena-dev` (a few lines below) edits
|
||||
// `package.json` to modify oclif paths, and this results in cache
|
||||
// invalidation and a performance hit rather than speedup.
|
||||
// * Even if the timestamps are removed from cacheKiller, so that there is no
|
||||
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
|
||||
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
|
||||
// * `fast-boot` causes unexpected behavior when used with `npm link` or
|
||||
// when the `node_modules` folder is manually modified (affecting transitive
|
||||
// dependencies) during development (e.g. bug investigations). A workaround
|
||||
// is to use `balena-dev` without `fast-boot`. See also notes in
|
||||
// `CONTRIBUTING.md`.
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
|
||||
@ -31,12 +47,6 @@ process.on('SIGINT', function () {
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheScope: __dirname + '/..',
|
||||
cacheFile: '.fast-boot.json',
|
||||
});
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
|
87
completion/_balena
Normal file
87
completion/_balena
Normal file
@ -0,0 +1,87 @@
|
||||
#compdef balena
|
||||
#autoload
|
||||
|
||||
#GENERATED FILE DON'T MODIFY#
|
||||
|
||||
_balena() {
|
||||
typeset -A opt_args
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util )
|
||||
# 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 )
|
||||
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_main_cmds] )) ||
|
||||
_balena_main_cmds() {
|
||||
_describe -t main_commands 'command' main_commands "$@" && ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_sec_cmds] )) ||
|
||||
_balena_sec_cmds() {
|
||||
case $line[1] in
|
||||
"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
|
||||
;;
|
||||
"device")
|
||||
_describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0
|
||||
;;
|
||||
"devices")
|
||||
_describe -t devices_cmds 'devices_cmd' devices_cmds "$@" && ret=0
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
"key")
|
||||
_describe -t key_cmds 'key_cmd' key_cmds "$@" && ret=0
|
||||
;;
|
||||
"local")
|
||||
_describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
|
||||
esac
|
||||
}
|
||||
|
||||
_balena "$@"
|
84
completion/balena-completion.bash
Normal file
84
completion/balena-completion.bash
Normal file
@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
|
||||
#GENERATED FILE DON'T MODIFY#
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util"
|
||||
# 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"
|
||||
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
api-key)
|
||||
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
|
||||
;;
|
||||
app)
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
config)
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
device)
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
devices)
|
||||
COMPREPLY=( $(compgen -W "$devices_cmds" -- $cur) )
|
||||
;;
|
||||
env)
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
fleet)
|
||||
COMPREPLY=( $(compgen -W "$fleet_cmds" -- $cur) )
|
||||
;;
|
||||
internal)
|
||||
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
|
||||
;;
|
||||
key)
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
local)
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
os)
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
release)
|
||||
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
||||
;;
|
||||
tag)
|
||||
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
||||
;;
|
||||
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
175
completion/generate-completion.js
Normal file
175
completion/generate-completion.js
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const fs = require('fs');
|
||||
const manifestFile = 'oclif.manifest.json';
|
||||
|
||||
commandsFilePath = path.join(rootDir, manifestFile);
|
||||
if (fs.existsSync(commandsFilePath)) {
|
||||
console.log('Generating shell auto completion files...');
|
||||
} else {
|
||||
console.error(`generate-completion.js: Could not find "${manifestFile}"`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
||||
|
||||
var mainCommands = [];
|
||||
var additionalCommands = [];
|
||||
for (const key of Object.keys(commandsJson.commands)) {
|
||||
const cmd = key.split(':');
|
||||
if (cmd.length > 1) {
|
||||
additionalCommands.push(cmd);
|
||||
if (!mainCommands.includes(cmd[0])) {
|
||||
mainCommands.push(cmd[0]);
|
||||
}
|
||||
} else {
|
||||
mainCommands.push(cmd[0]);
|
||||
}
|
||||
}
|
||||
const mainCommandsStr = mainCommands.join(' ');
|
||||
|
||||
// GENERATE BASH COMPLETION FILE
|
||||
bashFilePathIn = path.join(__dirname, '/templates/bash.template');
|
||||
bashFilePathOut = path.join(__dirname, 'balena-completion.bash');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(bashFilePathOut);
|
||||
} catch (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
fs.readFile(bashFilePathIn, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
process.exitCode = 1;
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
data = data.replace(
|
||||
'#TEMPLATE FILE FOR BASH COMPLETION#',
|
||||
"#GENERATED FILE DON'T MODIFY#",
|
||||
);
|
||||
|
||||
data = data.replace(
|
||||
/\$main_commands\$/g,
|
||||
'main_commands="' + mainCommandsStr + '"',
|
||||
);
|
||||
var subCommands = [];
|
||||
var prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
} else {
|
||||
const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds';
|
||||
data = data.replace(
|
||||
/\$sub_cmds\$/g,
|
||||
' ' + prevElement2 + '="' + subCommands.join(' ') + '"\n$sub_cmds$',
|
||||
);
|
||||
data = data.replace(
|
||||
/\$sub_cmds_prev\$/g,
|
||||
' ' +
|
||||
prevElement +
|
||||
')\n COMPREPLY=( $(compgen -W "$' +
|
||||
prevElement2 +
|
||||
'" -- $cur) )\n ;;\n$sub_cmds_prev$',
|
||||
);
|
||||
prevElement = element[0];
|
||||
subCommands = [];
|
||||
subCommands.push(element[1]);
|
||||
}
|
||||
});
|
||||
// cleanup placeholders
|
||||
data = data.replace(/\$sub_cmds\$/g, '');
|
||||
data = data.replace(/\$sub_cmds_prev\$/g, '');
|
||||
|
||||
fs.writeFile(bashFilePathOut, data, 'utf8', function (error) {
|
||||
if (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// GENERATE ZSH COMPLETION FILE
|
||||
zshFilePathIn = path.join(__dirname, '/templates/zsh.template');
|
||||
zshFilePathOut = path.join(__dirname, '_balena');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(zshFilePathOut);
|
||||
} catch (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
fs.readFile(zshFilePathIn, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
process.exitCode = 1;
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
data = data.replace(
|
||||
'#TEMPLATE FILE FOR ZSH COMPLETION#',
|
||||
"#GENERATED FILE DON'T MODIFY#",
|
||||
);
|
||||
|
||||
data = data.replace(
|
||||
/\$main_commands\$/g,
|
||||
'main_commands=( ' + mainCommandsStr + ' )',
|
||||
);
|
||||
var subCommands = [];
|
||||
var prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
} else {
|
||||
const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds';
|
||||
data = data.replace(
|
||||
/\$sub_cmds\$/g,
|
||||
' ' + prevElement2 + '=( ' + subCommands.join(' ') + ' )\n$sub_cmds$',
|
||||
);
|
||||
data = data.replace(
|
||||
/\$sub_cmds_prev\$/g,
|
||||
' "' +
|
||||
prevElement +
|
||||
'")\n _describe -t ' +
|
||||
prevElement2 +
|
||||
" '" +
|
||||
prevElement +
|
||||
"_cmd' " +
|
||||
prevElement2 +
|
||||
' "$@" && ret=0\n ;;\n$sub_cmds_prev$',
|
||||
);
|
||||
prevElement = element[0];
|
||||
subCommands = [];
|
||||
subCommands.push(element[1]);
|
||||
}
|
||||
});
|
||||
// cleanup placeholders
|
||||
data = data.replace(/\$sub_cmds\$/g, '');
|
||||
data = data.replace(/\$sub_cmds_prev\$/g, '');
|
||||
|
||||
fs.writeFile(zshFilePathOut, data, 'utf8', function (error) {
|
||||
if (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
32
completion/templates/bash.template
Normal file
32
completion/templates/bash.template
Normal file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
#TEMPLATE FILE FOR BASH COMPLETION#
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
$main_commands$
|
||||
# Sub-completions
|
||||
$sub_cmds$
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
$sub_cmds_prev$
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
35
completion/templates/zsh.template
Normal file
35
completion/templates/zsh.template
Normal file
@ -0,0 +1,35 @@
|
||||
#compdef balena
|
||||
#autoload
|
||||
|
||||
#TEMPLATE FILE FOR ZSH COMPLETION#
|
||||
|
||||
_balena() {
|
||||
typeset -A opt_args
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
$main_commands$
|
||||
# Sub-completions
|
||||
$sub_cmds$
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_main_cmds] )) ||
|
||||
_balena_main_cmds() {
|
||||
_describe -t main_commands 'command' main_commands "$@" && ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_sec_cmds] )) ||
|
||||
_balena_sec_cmds() {
|
||||
case $line[1] in
|
||||
$sub_cmds_prev$
|
||||
esac
|
||||
}
|
||||
|
||||
_balena "$@"
|
@ -7,7 +7,7 @@ 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
|
||||
balena device init --fleet FLEET_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
|
||||
|
||||
```
|
||||
|
||||
@ -24,7 +24,7 @@ But before you can run it you need to collect the parameters and build the confi
|
||||
```
|
||||
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. `FLEET_ID`. Create a fleet (`balena fleet create FLEET_NAME --type DEVICE_TYPE`) or find an existing one (`balena fleets`) and notice its ID.
|
||||
|
||||
1. `OS_VERSION`. Run
|
||||
```bash
|
||||
|
1443
doc/cli.markdown
1443
doc/cli.markdown
File diff suppressed because it is too large
Load Diff
410
docker/DOCKER.md
410
docker/DOCKER.md
@ -1,410 +0,0 @@
|
||||
# Docker Images for the balena CLI
|
||||
|
||||
Docker images with the balena CLI and Docker-in-Docker.
|
||||
|
||||
## Features Overview
|
||||
|
||||
These CLI images are based on the popular [Balena base images](https://www.balena.io/docs/reference/base-images/base-images/)
|
||||
so they include many of the features you see there.
|
||||
|
||||
- Multiple Architectures:
|
||||
- `rpi`
|
||||
- `armv7hf`
|
||||
- `aarch64` (debian only)
|
||||
- `amd64`
|
||||
- `i386`
|
||||
- Multiple Distributions
|
||||
- `debian`
|
||||
- `alpine`
|
||||
- [cross-build](https://www.balena.io/docs/reference/base-images/base-images/#building-arm-containers-on-x86-machines) functionality for building ARM containers on x86.
|
||||
- Helpful package installer script called `install_packages` inspired by [minideb](https://github.com/bitnami/minideb#why-use-minideb).
|
||||
|
||||
Note that there are some additional considerations when running the CLI via Docker so
|
||||
pay close attention to the [Usage](#usage) section for examples of different CLI commands.
|
||||
|
||||
## Image Names
|
||||
|
||||
`balenalib/<arch>-<distro>-balenacli:<cli_ver>`
|
||||
|
||||
- `<arch>` is the architecture and is mandatory. If using `Dockerfile.template`, you can replace this with `%%BALENA_ARCH%%`.
|
||||
For a list of available device names and architectures, see the [Device types](https://www.balena.io/docs/reference/base-images/devicetypes/).
|
||||
- `<distro>` is the Linux distribution and is mandatory. Currently there are 2 distributions, namely `debian` and `alpine`.
|
||||
|
||||
## Image Tags
|
||||
|
||||
In the tags, all of the fields are optional, and if they are left out, they will default to their `latest` pointer.
|
||||
|
||||
- `<cli_ver>` is the version of the balena CLI, for example, `12.40.2`, it can also be substituted for `latest`.
|
||||
|
||||
## Examples
|
||||
|
||||
`balenalib/amd64-debian-balenacli:12.40.2`
|
||||
|
||||
- `<arch>`: amd64 - suitable for running on most workstations
|
||||
- `<distro>`: debian - widely used base distro
|
||||
- `<cli_ver>`: 12.40.2
|
||||
|
||||
`balenalib/armv7hf-alpine-balenacli`
|
||||
|
||||
- `<arch>`: armv7hf - suitable for running on a Raspberry Pi 3 for example
|
||||
- `<distro>`: alpine - smaller footprint than debian
|
||||
- `<cli_ver>`: omitted - the latest available CLI version will be used
|
||||
|
||||
## Volumes
|
||||
|
||||
Volumes can be used to persist data between instances of the CLI container, or to share
|
||||
files between the host and the container.
|
||||
In most cases these are optional, but some examples will highlight when volumes are required.
|
||||
|
||||
- `-v "balena_data:/root/.balena"`: persist balena credentials and downloads between instances
|
||||
- `-v "docker_data:/var/lib/docker"`: persist cache between instances when using Docker-in-Docker (requires `-e "DOCKERD=1"`)
|
||||
- `-v "$PWD:$PWD" -w "$PWD"`: bind mount your current working directory in the container to share app sources or balenaOS image files
|
||||
- `-v "${SSH_AUTH_SOCK}:/ssh-agent"`: bind mount your host ssh-agent socket with preloaded SSH keys
|
||||
- `-v "/var/run/docker.sock:/var/run/docker.sock"`: bind mount your host Docker socket instead of Docker-in-Docker
|
||||
|
||||
## Environment Variables
|
||||
|
||||
These environment variables are available for additional functionality included in the CLI image.
|
||||
In most cases these are optional, but some examples will highlight when environment variables are required.
|
||||
|
||||
- `-e "SSH_PRIVATE_KEY=$(</path/to/priv/key)"`: copy your private SSH key file contents as an environment variable
|
||||
- `-e "DOCKERD=1"`: enable the included Docker-in-Docker daemon (requires `--cap-add SYS_ADMIN`)
|
||||
|
||||
## Keeping the CLI image up to date
|
||||
|
||||
Please note that using the `:latest` tag is not enough to keep the image up to date,
|
||||
because Docker will reuse a locally cached image. To update the image to the latest
|
||||
version, run:
|
||||
|
||||
```bash
|
||||
$ docker pull balenalib/<arch>-<distro>-balenacli
|
||||
```
|
||||
|
||||
Replacing `<arch>` and `<distro>` with the image architecture and distribution as
|
||||
described earlier.
|
||||
|
||||
If you are using Docker v19.09 or later, you can also add the `--pull always` flag to
|
||||
`docker run` commands, so that Docker automatically checks for available updates
|
||||
(new image layers will only be downloaded if a new version is available).
|
||||
|
||||
## Usage
|
||||
|
||||
We've provided some examples of common CLI commands and how they are best used
|
||||
with this image, since some special considerations must be made.
|
||||
|
||||
- [login](#login) - login to balena
|
||||
- [push](#push) - start a build on the remote balenaCloud build servers, or a local mode device
|
||||
- [logs](#logs) - show device logs
|
||||
- [ssh](#ssh) - SSH into the host or application container of a device
|
||||
- [apps](#app--apps) - list all applications
|
||||
- [app](#app--apps) - display information about a single application
|
||||
- [devices](#device--devices) - list all devices
|
||||
- [device](#device--devices) - show info about a single device
|
||||
- [tunnel](#tunnel) - tunnel local ports to your balenaOS device
|
||||
- [preload](#preload) - preload an app on a disk image (or Edison zip archive)
|
||||
- [build](#build--deploy) - build a project locally
|
||||
- [deploy](#build--deploy) - deploy a single image or a multicontainer project to a balena application
|
||||
- [join](#join--leave) - move a local device to an application on another balena server
|
||||
- [leave](#join--leave) - remove a local device from its balena application
|
||||
- [scan](#scan) - scan for balenaOS devices on your local network
|
||||
|
||||
For each example we have also linked to the corresponding sections of the
|
||||
balena CLI Documentation here: https://www.balena.io/docs/reference/balena-cli
|
||||
|
||||
### login
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#login>
|
||||
|
||||
The `balena login` command can't be used with web authorization and a browser
|
||||
when running in a container. Instead it must be used with `--token` or `--credentials`.
|
||||
|
||||
Notice that here we've used a named volume `balena_data` to store credentials
|
||||
for future runs of the CLI image. This is optional but avoids having to run the login
|
||||
command again every time you run the image.
|
||||
|
||||
```bash
|
||||
$ docker volume create balena_data
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena login --credentials --email "johndoe@gmail.com" --password "secret"
|
||||
> balena login --token "..."
|
||||
> exit
|
||||
```
|
||||
|
||||
### push
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#push-applicationordevice>
|
||||
|
||||
In this example we are mounting your current working directory into the container with `-v "$PWD:$PWD" -w "$PWD"`.
|
||||
This will bind mount your current working directory into the container at the same absolute path.
|
||||
|
||||
This bind mount is required so the CLI has access to your app sources.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "$PWD:$PWD" -w "$PWD" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena push myApp --source .
|
||||
> balena push 10.0.0.1 --env MY_ENV_VAR=value --env my-service:SERVICE_VAR=value
|
||||
> exit
|
||||
```
|
||||
|
||||
### logs
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#logs-device>
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena logs 23c73a1 --service my-service
|
||||
> balena logs 23c73a1.local --system --tail
|
||||
> exit
|
||||
```
|
||||
|
||||
### ssh
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#key-add-name-path>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#ssh-applicationordevice-service>
|
||||
|
||||
The `balena ssh` command requires an existing SSH key added to your balenaCloud
|
||||
account.
|
||||
|
||||
One way to make this key available to the container is to pass the private key file contents as an environment variable.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-e "SSH_PRIVATE_KEY=$(</path/to/priv/key)" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena ssh f49cefd
|
||||
> balena ssh f49cefd my-service
|
||||
> balena ssh 192.168.0.1 --verbose
|
||||
> exit
|
||||
```
|
||||
|
||||
Another way to share SSH keys with the container is to mount your SSH agent socket with keys preloaded.
|
||||
|
||||
```bash
|
||||
$ eval ssh-agent
|
||||
$ ssh-add /path/to/priv/key
|
||||
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "${SSH_AUTH_SOCK}:/ssh-agent" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena ssh f49cefd
|
||||
> balena ssh f49cefd my-service
|
||||
> balena ssh 192.168.0.1 --verbose
|
||||
> exit
|
||||
```
|
||||
|
||||
### app | apps
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#app-nameorslug>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#apps>
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena apps
|
||||
> balena app myorg/myapp
|
||||
> exit
|
||||
```
|
||||
|
||||
### device | devices
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#device-uuid>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#devices>
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena devices --application MyApp
|
||||
> balena device 7cf02a6
|
||||
> exit
|
||||
```
|
||||
|
||||
### tunnel
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#tunnel-deviceorapplication>
|
||||
|
||||
The `balena tunnel` command is easiest used when the host networking stack
|
||||
can be shared with the container and ports can be easily assigned.
|
||||
|
||||
However the host networking driver only works on Linux hosts, and is not supported
|
||||
on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.
|
||||
|
||||
Instead you can bind specific port ranges to the host so you can access the tunnel
|
||||
from outside the container via `localhost:[localPort]`.
|
||||
|
||||
Note that when exposing individual ports, you must specify all interfaces in the format
|
||||
`[remotePort]:0.0.0.0:[localPort]` otherwise the tunnel will only be listening for
|
||||
connections within the container.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-p 22222:22222 \
|
||||
-p 12345:54321
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena tunnel 2ead211 -p 22222:0.0.0.0
|
||||
> balena tunnel myApp -p 54321:0.0.0.0:12345
|
||||
> exit
|
||||
```
|
||||
|
||||
If you have host networking available then you do not need to specify ports
|
||||
in your run command, and the interface `0.0.0.0` is optional in your tunnel command.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
--network host \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena tunnel 2ead211 -p 22222
|
||||
> balena tunnel myApp -p 54321:12345
|
||||
> exit
|
||||
```
|
||||
|
||||
|
||||
|
||||
### preload
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#os-download-type>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#os-configure-image>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#preload-image>
|
||||
|
||||
The `balena preload` command requires access to a Docker client and daemon.
|
||||
|
||||
The easiest way to run this command is to use the included Docker-in-Docker daemon.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "docker_data:/var/lib/docker" \
|
||||
-e "DOCKERD=1" --cap-add SYS_ADMIN \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena os download raspberrypi3 -o raspberry-pi.img
|
||||
> balena os configure raspberry-pi.img --app MyApp
|
||||
> balena preload raspberry-pi.img --app MyApp --commit current
|
||||
> exit
|
||||
```
|
||||
|
||||
Another way to run the `preload` command is to use the host OS Docker socket and avoid
|
||||
starting a Docker daemon in the container. This is achieved with `-v "/var/run/docker.sock:/var/run/docker.sock"`.
|
||||
|
||||
In this example we are mounting your current working directory into the container with `-v "$PWD:$PWD" -w "$PWD"`.
|
||||
This will bind mount your current working directory into the container at the same absolute path.
|
||||
|
||||
This bind mount is required when using the host Docker socket because the absolute path to the balenaOS image
|
||||
file must be the same from both the perspective of the CLI in the container and the host Docker socket.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock" \
|
||||
-v "$PWD:$PWD" -w "$PWD" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena os download raspberrypi3 -o raspberry-pi.img
|
||||
> balena os configure raspberry-pi.img --app MyApp
|
||||
> balena preload raspberry-pi.img --app MyApp --commit current
|
||||
> exit
|
||||
```
|
||||
|
||||
### build | deploy
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#build-source>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#deploy-appname-image>
|
||||
|
||||
The `build` and `deploy` commands both require access to a Docker client and daemon.
|
||||
|
||||
The easiest way to run these commands is to use the included Docker-in-Docker daemon.
|
||||
|
||||
In this example we are mounting your current working directory into the container with `-v "$PWD:$PWD" -w "$PWD"`.
|
||||
This will bind mount your current working directory into the container at the same absolute path.
|
||||
|
||||
This bind mount is required so the CLI has access to your app sources.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "docker_data:/var/lib/docker" \
|
||||
-e DOCKERD=1 --cap-add SYS_ADMIN \
|
||||
-v "$PWD:$PWD" -w "$PWD" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena build --application myApp
|
||||
> balena deploy myApp
|
||||
> exit
|
||||
```
|
||||
|
||||
Another way to run the `build` and `deploy` commands is to use the host OS Docker socket and avoid
|
||||
starting a Docker daemon in the container. This is achieved with `-v "/var/run/docker.sock:/var/run/docker.sock"`.
|
||||
|
||||
In this example we are mounting your current working directory into the container with `-v "$PWD:$PWD" -w "$PWD"`.
|
||||
This will bind mount your current working directory into the container at the same absolute path.
|
||||
|
||||
This bind mount is required so the CLI has access to your app sources.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock" \
|
||||
-v "$PWD:$PWD" -w "$PWD" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena build --application myApp
|
||||
> balena deploy myApp
|
||||
> exit
|
||||
```
|
||||
|
||||
### join | leave
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#join-deviceiporhostname>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#leave-deviceiporhostname>
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena join balena.local --application MyApp
|
||||
> balena leave balena.local
|
||||
> exit
|
||||
```
|
||||
|
||||
### scan
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#scan>
|
||||
|
||||
The `balena scan` command requires access to the host network interface
|
||||
in order to bind and listen for multicast responses from devices.
|
||||
|
||||
However the host networking driver only works on Linux hosts, and is not supported
|
||||
on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it --network host balenalib/amd64-debian-balenacli scan
|
||||
```
|
||||
|
||||
## Custom images / contributing
|
||||
|
||||
The following steps may be used to create custom CLI images or
|
||||
to contribute bug reports, fixes or features.
|
||||
|
||||
```bash
|
||||
# the currently supported base images are 'debian' and 'alpine'
|
||||
export BALENA_DISTRO="debian"
|
||||
|
||||
# provide the architecture where you will be testing the image
|
||||
export BALENA_ARCH="amd64"
|
||||
|
||||
# optionally register QEMU binfmt if building for other architectures (eg. armv7hf)
|
||||
$ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
# build and tag an image with docker
|
||||
docker build . -f docker/${BALENA_DISTRO}/Dockerfile \
|
||||
--build-arg "BUILD_BASE=balenalib/${BALENA_ARCH}-${BALENA_DISTRO}-node:12.19.1-build" \
|
||||
--build-arg "RUN_BASE=balenalib/${BALENA_ARCH}-${BALENA_DISTRO}-node:12.19.1-run" \
|
||||
--tag "balenalib/${BALENA_ARCH}-${BALENA_DISTRO}-balenacli"
|
||||
```
|
@ -1,43 +0,0 @@
|
||||
ARG BUILD_BASE=balenalib/amd64-alpine-node:12.19.1-build
|
||||
ARG RUN_BASE=balenalib/amd64-alpine-node:12.19.1-run
|
||||
|
||||
FROM ${BUILD_BASE} as build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
# dev dependencies are required for build:fast
|
||||
# --unsafe-perm is not needed because of global /usr/local/etc/npmrc
|
||||
RUN npm install
|
||||
|
||||
RUN npm run build:fast
|
||||
|
||||
# remove dev dependencies after build:fast
|
||||
RUN npm prune --production
|
||||
|
||||
FROM ${RUN_BASE}
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY --from=build /usr/src/app/ .
|
||||
|
||||
ENV PATH $PATH:/usr/src/app/bin
|
||||
|
||||
# fail early if balena binary won't run
|
||||
RUN balena --version
|
||||
|
||||
# https://github.com/balena-io/balena-cli/blob/master/INSTALL-LINUX.md#additional-dependencies
|
||||
RUN install_packages avahi bash ca-certificates docker jq openssh
|
||||
|
||||
COPY docker/docker-init.sh init.sh
|
||||
|
||||
RUN CLI_CMDS=$(jq -r '.commands | keys | map(.[0:index(":")]) | unique | join("\\ ")' < oclif.manifest.json); \
|
||||
sed -ie "s/CLI_CMDS=\"help\"/CLI_CMDS=\"help\\ ${CLI_CMDS}\"/" init.sh && \
|
||||
chmod +x init.sh
|
||||
|
||||
ENTRYPOINT [ "/usr/src/app/init.sh" ]
|
||||
|
||||
CMD [ "help" ]
|
||||
|
||||
ENV SSH_AUTH_SOCK "/ssh-agent"
|
@ -1,43 +0,0 @@
|
||||
ARG BUILD_BASE=balenalib/amd64-debian-node:12.19.1-build
|
||||
ARG RUN_BASE=balenalib/amd64-debian-node:12.19.1-run
|
||||
|
||||
FROM ${BUILD_BASE} as build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
# dev dependencies are required for build:fast
|
||||
# --unsafe-perm is not needed because of global /usr/local/etc/npmrc
|
||||
RUN npm install
|
||||
|
||||
RUN npm run build:fast
|
||||
|
||||
# remove dev dependencies after build:fast
|
||||
RUN npm prune --production
|
||||
|
||||
FROM ${RUN_BASE}
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY --from=build /usr/src/app/ .
|
||||
|
||||
ENV PATH $PATH:/usr/src/app/bin
|
||||
|
||||
# fail early if balena binary won't run
|
||||
RUN balena --version
|
||||
|
||||
# https://github.com/balena-io/balena-cli/blob/master/INSTALL-LINUX.md#additional-dependencies
|
||||
RUN install_packages avahi-daemon ca-certificates docker.io jq openssh-client
|
||||
|
||||
COPY docker/docker-init.sh init.sh
|
||||
|
||||
RUN CLI_CMDS=$(jq -r '.commands | keys | map(.[0:index(":")]) | unique | join("\\ ")' < oclif.manifest.json); \
|
||||
sed -ie "s/CLI_CMDS=\"help\"/CLI_CMDS=\"help\\ ${CLI_CMDS}\"/" init.sh && \
|
||||
chmod +x init.sh
|
||||
|
||||
ENTRYPOINT [ "/usr/src/app/init.sh" ]
|
||||
|
||||
CMD [ "help" ]
|
||||
|
||||
ENV SSH_AUTH_SOCK "/ssh-agent"
|
@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# start dockerd if env var is set
|
||||
if [ "${DOCKERD}" = "1" ]
|
||||
then
|
||||
[ -e /var/run/docker.sock ] && rm /var/run/docker.sock
|
||||
dockerd &
|
||||
fi
|
||||
|
||||
# load private ssh key if one is provided
|
||||
if [ -n "${SSH_PRIVATE_KEY}" ]
|
||||
then
|
||||
# if an ssh agent socket was not provided, start our own agent
|
||||
[ -e "${SSH_AUTH_SOCK}" ] || eval "$(ssh-agent -s)"
|
||||
echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add -
|
||||
fi
|
||||
|
||||
# space-separated list of balena CLI commands (filled in through `sed`
|
||||
# in a Dockerfile RUN instruction)
|
||||
CLI_CMDS="help"
|
||||
|
||||
# treat the provided command as a balena CLI arg...
|
||||
# 1. if the first word matches a known entry in CLI_CMDS
|
||||
# 2. OR if the first character is a hyphen (eg. -h or --debug)
|
||||
if [[ " ${CLI_CMDS} " =~ " ${1} " ]] || [ "${1:0:1}" = "-" ]
|
||||
then
|
||||
exec balena "$@"
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
91
lib/app.ts
91
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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,37 +95,59 @@ async function init() {
|
||||
}
|
||||
|
||||
/** Execute the oclif parser and the CLI command. */
|
||||
async function oclifRun(
|
||||
command: string[],
|
||||
options: import('./preparser').AppOptions,
|
||||
) {
|
||||
const { CustomMain } = await import('./utils/oclif-utils');
|
||||
const runPromise = CustomMain.run(command).then(
|
||||
() => {
|
||||
if (!options.noFlush) {
|
||||
return require('@oclif/command/flush');
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// oclif sometimes exits with ExitError code 0 (not an error)
|
||||
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;
|
||||
try {
|
||||
await CustomMain.run(command);
|
||||
} catch (error) {
|
||||
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
||||
// for example the `balena help` command.
|
||||
// (Avoid `error instanceof ExitError` here for the reasons explained
|
||||
// in the CONTRIBUTING.md file regarding the `instanceof` operator.)
|
||||
if (error.oclif?.exit === 0) {
|
||||
return;
|
||||
isEEXIT = true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (shouldFlush) {
|
||||
await import('@oclif/command/flush');
|
||||
}
|
||||
// TODO: figure out why we need to call fast-boot stop() here, in
|
||||
// addition to calling it in the main `run()` function in this file.
|
||||
// If it is not called here as well, there is a process exit delay of
|
||||
// 1 second when the fast-boot2 cache is modified (1 second is the
|
||||
// default cache saving timeout). Try for example `balena help`.
|
||||
// I have found that, when oclif's `Error: EEXIT: 0` is caught in
|
||||
// the try/catch block above, execution does not get past the
|
||||
// Promise.all() call below, but I don't understand why.
|
||||
if (isEEXIT) {
|
||||
(await import('./fast-boot')).stop();
|
||||
}
|
||||
})(!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');
|
||||
normalizeEnvVars();
|
||||
@ -136,8 +160,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));
|
||||
|
||||
@ -146,6 +168,13 @@ export async function run(
|
||||
} catch (err) {
|
||||
await (await import('./errors')).handleError(err);
|
||||
} finally {
|
||||
try {
|
||||
(await import('./fast-boot')).stop();
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Stopping fast-boot: ${e}`);
|
||||
}
|
||||
}
|
||||
// Windows fix: reading from stdin prevents the process from exiting
|
||||
process.stdin.pause();
|
||||
}
|
||||
|
@ -59,14 +59,15 @@ export class LoginServer extends EventEmitter {
|
||||
app.set('views', path.join(__dirname, 'pages'));
|
||||
|
||||
this.server = await new Promise<import('net').Server>((resolve, reject) => {
|
||||
const server = app.listen(port, host, (err: Error) => {
|
||||
const callback = (err: Error) => {
|
||||
if (err) {
|
||||
this.emit('error', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(server);
|
||||
}
|
||||
});
|
||||
};
|
||||
const server = app.listen(port, host, callback as any);
|
||||
server.on('connection', (socket) => this.serverSockets.push(socket));
|
||||
});
|
||||
|
||||
|
@ -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,14 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
import type { Application } from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import type { Application } from 'balena-sdk';
|
||||
import { appToFleetCmdMsg, warnify } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
@ -32,18 +35,18 @@ interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppCreateCmd extends Command {
|
||||
export 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,41 +58,39 @@ 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,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppCreateCmd,
|
||||
);
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params, flags: options } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCreateCmd);
|
||||
|
||||
// Ascertain device type
|
||||
const deviceType =
|
||||
@ -112,12 +113,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}".`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -128,8 +129,8 @@ export default class AppCreateCmd extends Command {
|
||||
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}"`
|
||||
: `Fleet created: ${application.slug} (${deviceType}, id ${application.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -150,3 +151,31 @@ export default class AppCreateCmd extends Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppCreateCmd extends FleetCreateCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet create' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet create'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app create <name>';
|
||||
public static args = FleetCreateCmd.args;
|
||||
public static flags = FleetCreateCmd.flags;
|
||||
public static authenticated = FleetCreateCmd.authenticated;
|
||||
public static primary = FleetCreateCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCreateCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
||||
|
@ -1,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,35 +15,44 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
import type { Release } from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import type { Release } from 'balena-sdk';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class AppCmd extends Command {
|
||||
export 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,
|
||||
@ -52,21 +61,18 @@ export default class AppCmd extends Command {
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppCmd);
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const application = (await getApplication(
|
||||
getBalenaSdk(),
|
||||
params.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;
|
||||
@ -88,3 +94,31 @@ export default class AppCmd extends Command {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppCmd extends FleetCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app <fleet>';
|
||||
public static args = FleetCmd.args;
|
||||
public static flags = FleetCmd.flags;
|
||||
public static authenticated = FleetCmd.authenticated;
|
||||
public static primary = FleetCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
||||
|
@ -15,39 +15,45 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class AppPurgeCmd extends Command {
|
||||
export 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,
|
||||
@ -55,8 +61,9 @@ export default class AppPurgeCmd extends Command {
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
@ -64,7 +71,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);
|
||||
@ -78,3 +85,31 @@ export default class AppPurgeCmd extends Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppPurgeCmd extends FleetPurgeCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet purge' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet purge'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app purge <fleet>';
|
||||
public static args = FleetPurgeCmd.args;
|
||||
public static flags = FleetPurgeCmd.flags;
|
||||
public static authenticated = FleetPurgeCmd.authenticated;
|
||||
public static primary = FleetPurgeCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
||||
|
@ -15,28 +15,34 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
import type { ApplicationType } from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import type { ApplicationType } from 'balena-sdk';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
newName?: string;
|
||||
}
|
||||
|
||||
export default class AppRenameCmd extends Command {
|
||||
export 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 +51,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,
|
||||
@ -66,8 +72,9 @@ export default class AppRenameCmd extends Command {
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
|
||||
|
||||
const { validateApplicationName } = await import('../../utils/validation');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
@ -76,7 +83,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 +93,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,7 +108,7 @@ 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,
|
||||
})) ||
|
||||
@ -115,9 +120,7 @@ export default class AppRenameCmd extends Command {
|
||||
} catch (e) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
if ((e.message || '').toLowerCase().includes('unique')) {
|
||||
throw new ExpectedError(
|
||||
`Error: application ${params.application} already exists.`,
|
||||
);
|
||||
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@ -128,7 +131,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}`);
|
||||
@ -137,3 +140,31 @@ export default class AppRenameCmd extends Command {
|
||||
console.log(`\tslug: ${renamedApplication.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppRenameCmd extends FleetRenameCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet rename' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet rename'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app rename <fleet> [newName]';
|
||||
public static args = FleetRenameCmd.args;
|
||||
public static flags = FleetRenameCmd.flags;
|
||||
public static authenticated = FleetRenameCmd.authenticated;
|
||||
public static primary = FleetRenameCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
||||
|
@ -15,38 +15,44 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class AppRestartCmd extends Command {
|
||||
export 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,
|
||||
@ -54,16 +60,45 @@ export default class AppRestartCmd extends Command {
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRestartCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const application = await getApplication(balena, params.application);
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
await balena.models.application.restart(application.id);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppRestartCmd extends FleetRestartCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet restart' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet restart'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app restart <fleet>';
|
||||
public static args = FleetRestartCmd.args;
|
||||
public static flags = FleetRestartCmd.flags;
|
||||
public static authenticated = FleetRestartCmd.authenticated;
|
||||
public static primary = FleetRestartCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
@ -28,14 +34,14 @@ interface FlagsDef {
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class AppRmCmd extends Command {
|
||||
export 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 +49,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,
|
||||
@ -59,10 +65,9 @@ export default class AppRmCmd extends Command {
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppRmCmd,
|
||||
);
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params, flags: options } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRmCmd);
|
||||
|
||||
const { confirm } = await import('../../utils/patterns');
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
@ -71,13 +76,41 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppRmCmd extends FleetRmCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleet rm' command
|
||||
|
||||
${appToFleetCmdMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleet rm'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'app rm <fleet>';
|
||||
public static args = FleetRmCmd.args;
|
||||
public static flags = FleetRmCmd.flags;
|
||||
public static authenticated = FleetRmCmd.authenticated;
|
||||
public static primary = FleetRmCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRmCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetCmdMsg));
|
||||
}
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
||||
|
@ -1,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,9 +16,12 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { appToFleetCmdMsg, warnify } from '../utils/messages';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
device_count?: number;
|
||||
@ -27,22 +30,22 @@ interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
verbose: boolean;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export default class AppsCmd extends Command {
|
||||
export class FleetsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all applications.
|
||||
List all fleets.
|
||||
|
||||
list all your balena applications.
|
||||
List all your balena fleets.
|
||||
|
||||
For detailed information on a particular application,
|
||||
use \`balena app <application>\` instead.
|
||||
For detailed information on a particular fleet, use
|
||||
\`balena fleet <fleet>\`
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena apps'];
|
||||
public static examples = ['$ balena fleets'];
|
||||
|
||||
public static usage = 'apps';
|
||||
public static usage = 'fleets';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
@ -56,8 +59,10 @@ export default class AppsCmd extends Command {
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(AppsCmd);
|
||||
protected useAppWord = false;
|
||||
|
||||
public async run(_parserOutput?: ParserOutput<FlagsDef, {}>) {
|
||||
_parserOutput ||= this.parse<FlagsDef, {}>(FleetsCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
@ -85,7 +90,7 @@ export default class AppsCmd extends Command {
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
'app_name',
|
||||
this.useAppWord ? 'app_name' : 'app_name => NAME',
|
||||
'slug',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
@ -94,3 +99,36 @@ export default class AppsCmd extends Command {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const appsToFleetsRenameMsg = appToFleetCmdMsg
|
||||
.replace(/'app'/g, "'apps'")
|
||||
.replace(/'fleet'/g, "'fleets'");
|
||||
|
||||
export default class AppsCmd extends FleetsCmd {
|
||||
public static description = stripIndent`
|
||||
DEPRECATED alias for the 'fleets' command
|
||||
|
||||
${appsToFleetsRenameMsg
|
||||
.split('\n')
|
||||
.map((l) => `\t\t${l}`)
|
||||
.join('\n')}
|
||||
|
||||
For command usage, see 'balena help fleets'
|
||||
`;
|
||||
public static examples = [];
|
||||
public static usage = 'apps';
|
||||
public static args = FleetsCmd.args;
|
||||
public static flags = FleetsCmd.flags;
|
||||
public static authenticated = FleetsCmd.authenticated;
|
||||
public static primary = FleetsCmd.primary;
|
||||
|
||||
public async run() {
|
||||
// call this.parse() before deprecation message to parse '-h'
|
||||
const parserOutput = this.parse<FlagsDef, {}>(AppsCmd);
|
||||
if (process.stderr.isTTY) {
|
||||
console.error(warnify(appsToFleetsRenameMsg));
|
||||
}
|
||||
this.useAppWord = true;
|
||||
await super.run(parserOutput);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
@ -18,18 +18,27 @@
|
||||
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 { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import {
|
||||
appToFleetFlagMsg,
|
||||
buildArgDeprecation,
|
||||
dockerignoreHelp,
|
||||
registrySecretsHelp,
|
||||
warnify,
|
||||
} from '../utils/messages';
|
||||
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
||||
import { buildProject, composeCliFlags } from '../utils/compose_ts';
|
||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
import { isV13 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
arch?: string;
|
||||
deviceType?: string;
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
source?: string; // Not part of command profile - source param copied here.
|
||||
help: void;
|
||||
}
|
||||
@ -47,7 +56,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,
|
||||
@ -61,12 +70,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 = [
|
||||
@ -87,10 +96,8 @@ ${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',
|
||||
}),
|
||||
...(isV13() ? {} : { application: cf.application }),
|
||||
fleet: cf.fleet,
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
@ -105,10 +112,13 @@ ${dockerignoreHelp}
|
||||
BuildCmd,
|
||||
);
|
||||
|
||||
if (options.application && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.fleet;
|
||||
|
||||
await Command.checkLoggedInIf(!!options.application);
|
||||
|
||||
// compositions with many services trigger misleading warnings
|
||||
// @ts-ignore editing property that isn't typed but does exist
|
||||
(await import('events')).defaultMaxListeners = 1000;
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
@ -122,6 +132,11 @@ ${dockerignoreHelp}
|
||||
|
||||
await this.validateOptions(options, sdk);
|
||||
|
||||
// Build args are under consideration for removal - warn user
|
||||
if (options.buildArg) {
|
||||
console.log(buildArgDeprecation);
|
||||
}
|
||||
|
||||
const app = await this.getAppAndResolveArch(options);
|
||||
|
||||
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
|
||||
@ -153,7 +168,7 @@ ${dockerignoreHelp}
|
||||
) {
|
||||
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)',
|
||||
);
|
||||
}
|
||||
|
||||
@ -211,7 +226,7 @@ ${dockerignoreHelp}
|
||||
* @param opts
|
||||
*/
|
||||
protected async buildProject(
|
||||
docker: import('docker-toolbelt'),
|
||||
docker: import('dockerode'),
|
||||
logger: import('../utils/logger'),
|
||||
composeOpts: ComposeOpts,
|
||||
opts: {
|
||||
@ -224,7 +239,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 (
|
||||
@ -233,7 +253,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.',
|
||||
);
|
||||
}
|
||||
@ -251,7 +271,7 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
nogitignore: composeOpts.nogitignore, // v13: delete this line
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
}
|
||||
|
@ -19,13 +19,19 @@ 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,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import type { PineDeferred } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
version: string; // OS version
|
||||
application?: string;
|
||||
app?: string; // application alias
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
deviceApiKey?: string;
|
||||
deviceType?: string;
|
||||
@ -43,16 +49,15 @@ 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.
|
||||
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.
|
||||
|
||||
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 avoid interactive questions, specify a command line option for each question that
|
||||
would otherwise be asked.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
@ -62,11 +67,11 @@ export default class ConfigGenerateCmd extends Command {
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <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 MyFleet --version 2.12.7',
|
||||
'$ balena config generate --fleet myorg/myfleet --version 2.12.7',
|
||||
'$ balena config generate --fleet MyFleet --version 2.12.7 --deviceType fincm3',
|
||||
'$ balena config generate --fleet MyFleet --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet MyFleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
||||
];
|
||||
|
||||
public static usage = 'config generate';
|
||||
@ -76,20 +81,28 @@ 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'],
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device'],
|
||||
},
|
||||
app: { ...cf.app, exclusive: ['application', 'fleet', 'device'] },
|
||||
appUpdatePollInterval: flags.string({
|
||||
description: 'DEPRECATED alias for --updatePollInterval',
|
||||
}),
|
||||
}),
|
||||
fleet: { ...cf.fleet, exclusive: ['application', 'app', 'device'] },
|
||||
device: { ...cf.device, exclusive: ['application', 'app', 'fleet'] },
|
||||
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 +126,7 @@ 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 variable updates)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
@ -143,8 +156,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;
|
||||
@ -177,7 +190,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.application}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -218,7 +231,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,13 +239,16 @@ 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');
|
||||
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
if (options.device == null && options.application == null) {
|
||||
|
@ -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.
|
||||
@ -34,8 +34,11 @@ 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 the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
Inject a config.json file to a mounted filesystem, e.g. the SD card of a
|
||||
provisioned device or balenaOS image.
|
||||
|
||||
Note: if using a private/custom device type, please ensure you are logged in
|
||||
('balena login' command). Public device types do not require logging in.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
@ -54,21 +57,11 @@ export default class ConfigInjectCmd extends Command {
|
||||
public static usage = 'config inject <file>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
type: cf.deviceType,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
@ -76,12 +69,11 @@ export default class ConfigInjectCmd extends Command {
|
||||
ConfigInjectCmd,
|
||||
);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device/OS drive'));
|
||||
await umountAsync(drive);
|
||||
await safeUmount(drive);
|
||||
|
||||
const fs = await import('fs');
|
||||
const configJSON = JSON.parse(
|
||||
|
@ -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.
|
||||
@ -42,16 +42,8 @@ export default class ConfigReadCmd extends Command {
|
||||
public static usage = 'config read';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
type: cf.deviceType,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -62,12 +54,11 @@ export default class ConfigReadCmd extends Command {
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device drive'));
|
||||
await umountAsync(drive);
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const configJSON = await config.read(drive, options.type);
|
||||
|
@ -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.
|
||||
@ -42,16 +42,8 @@ export default class ConfigReconfigureCmd extends Command {
|
||||
public static usage = 'config reconfigure';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
type: cf.deviceType,
|
||||
drive: cf.driveOrImg,
|
||||
advanced: flags.boolean({
|
||||
description: 'show advanced commands',
|
||||
char: 'v',
|
||||
@ -66,16 +58,15 @@ export default class ConfigReconfigureCmd extends Command {
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device drive'));
|
||||
await umountAsync(drive);
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const { uuid } = await config.read(drive, options.type);
|
||||
await umountAsync(drive);
|
||||
await safeUmount(drive);
|
||||
|
||||
const configureCommand = ['os', 'configure', drive, '--device', uuid];
|
||||
if (options.advanced) {
|
||||
|
@ -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.
|
||||
@ -61,16 +61,8 @@ export default class ConfigWriteCmd extends Command {
|
||||
public static usage = 'config write <key> <value>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
type: cf.deviceType,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -83,24 +75,33 @@ export default class ConfigWriteCmd extends Command {
|
||||
ConfigWriteCmd,
|
||||
);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
const { denyMount, safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device drive'));
|
||||
await umountAsync(drive);
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const configJSON = await config.read(drive, options.type);
|
||||
|
||||
console.info(`Setting ${params.key} to ${params.value}`);
|
||||
const _ = await import('lodash');
|
||||
_.set(configJSON, params.key, params.value);
|
||||
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
|
||||
|
||||
await umountAsync(drive);
|
||||
|
||||
await config.write(drive, options.type, configJSON);
|
||||
await denyMount(drive, async () => {
|
||||
await safeUmount(drive);
|
||||
await config.write(drive, options.type, configJSON);
|
||||
});
|
||||
|
||||
console.info('Done');
|
||||
}
|
||||
|
||||
/** Call Lodash's _.setWith(). Moved here for ease of testing. */
|
||||
static updateConfigJson(configJSON: object, key: string, value: string) {
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
// note: _.setWith() is needed instead of _.set() because, given a key
|
||||
// like `os.udevRules.101`, _.set() creates a udevRules array (rather
|
||||
// than a dictionary) and sets the 101st array element to value, while
|
||||
// we actually want udevRules to be dictionary like { '101': value }
|
||||
_.setWith(configJSON, key, value, (v) => v || {});
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
@ -21,7 +21,12 @@ import type { ImageDescriptor } from 'resin-compose-parse';
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import {
|
||||
dockerignoreHelp,
|
||||
registrySecretsHelp,
|
||||
buildArgDeprecation,
|
||||
} from '../utils/messages';
|
||||
import * as ca from '../utils/common-args';
|
||||
import * as compose from '../utils/compose';
|
||||
import type {
|
||||
BuiltImage,
|
||||
@ -29,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,
|
||||
@ -54,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.)
|
||||
@ -77,13 +83,15 @@ the image in the balenaCloud build servers.)
|
||||
Unless an image is specified, this command will look into the current directory
|
||||
(or the one specified by --source) for a docker-compose.yml file. If one is
|
||||
found, this command will deploy each service defined in the compose file,
|
||||
building it first if an image for it doesn't exist. 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.
|
||||
building it first if an image for it doesn't exist. Image names will be looked
|
||||
up according to the scheme: \`<projectName>_<serviceName>\`.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
\`balena deploy <appOwnerUsername>/<appName>\`.
|
||||
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 a fleet where you are a collaborator, use fleet slug including the
|
||||
organization: \`balena deploy <organization>/<fleet>\`.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
@ -91,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({
|
||||
@ -127,12 +131,20 @@ ${dockerignoreHelp}
|
||||
}),
|
||||
'release-tag': flags.string({
|
||||
description: stripIndent`
|
||||
Set release tags if the image deployment is successful. Multiple
|
||||
Set release tags if the image deployment is successful. 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,
|
||||
}),
|
||||
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
|
||||
@ -149,14 +161,17 @@ ${dockerignoreHelp}
|
||||
DeployCmd,
|
||||
);
|
||||
|
||||
// compositions with many services trigger misleading warnings
|
||||
// @ts-ignore editing property that isn't typed but does exist
|
||||
(await import('events')).defaultMaxListeners = 1000;
|
||||
|
||||
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) {
|
||||
console.log(buildArgDeprecation);
|
||||
}
|
||||
|
||||
if (image != null && options.build) {
|
||||
throw new ExpectedError(
|
||||
@ -179,21 +194,19 @@ ${dockerignoreHelp}
|
||||
options['registry-secrets'],
|
||||
);
|
||||
} else {
|
||||
const {
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
} = await validateProjectDirectory(sdk, {
|
||||
dockerfilePath: options.dockerfile,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
projectPath: options.source || '.',
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
});
|
||||
const { dockerfilePath, registrySecrets } =
|
||||
await validateProjectDirectory(sdk, {
|
||||
dockerfilePath: options.dockerfile,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
projectPath: options.source || '.',
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
});
|
||||
options.dockerfile = dockerfilePath;
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
}
|
||||
|
||||
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([
|
||||
@ -204,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(
|
||||
@ -220,7 +234,7 @@ ${dockerignoreHelp}
|
||||
}
|
||||
|
||||
async deployProject(
|
||||
docker: import('docker-toolbelt'),
|
||||
docker: import('dockerode'),
|
||||
logger: import('../utils/logger'),
|
||||
composeOpts: ComposeOpts,
|
||||
opts: {
|
||||
@ -231,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');
|
||||
@ -244,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!',
|
||||
);
|
||||
}
|
||||
|
||||
@ -299,7 +319,7 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
nogitignore: composeOpts.nogitignore, // v13: delete this line
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
||||
@ -319,7 +339,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);
|
||||
|
||||
@ -363,6 +383,8 @@ ${dockerignoreHelp}
|
||||
`Bearer ${auth}`,
|
||||
apiEndpoint,
|
||||
!opts.shouldUploadLogs,
|
||||
composeOpts.projectPath,
|
||||
opts.createAsDraft,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,10 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { appToFleetOutputMsg, warnify } from '../../utils/messages';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
import type { Application, Release } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
@ -43,6 +46,7 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
v13: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -70,13 +74,17 @@ export default class DeviceCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
v13: cf.v13,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceCmd,
|
||||
);
|
||||
const useAppWord = !options.v13 && !isV13();
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
@ -111,7 +119,8 @@ export default class DeviceCmd extends Command {
|
||||
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
const belongsToApplication =
|
||||
device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
@ -161,6 +170,10 @@ export default class DeviceCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
if (useAppWord && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetOutputMsg));
|
||||
}
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
@ -171,7 +184,7 @@ export default class DeviceCmd extends Command {
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
'application_name',
|
||||
useAppWord ? 'application_name' : 'application_name => FLEET',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
|
@ -19,12 +19,18 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { runCommand } from '../../utils/helpers';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
yes: boolean;
|
||||
advanced: boolean;
|
||||
'os-version'?: string;
|
||||
@ -37,26 +43,30 @@ 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
|
||||
Initialize a device by downloading the OS image of the specified fleet
|
||||
and writing it to an SD Card.
|
||||
|
||||
Note, if the application option is omitted it will be prompted
|
||||
for interactively.
|
||||
If the --fleet option is omitted, it will be prompted for interactively.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena device init',
|
||||
'$ balena device init --application MyApp',
|
||||
'$ balena device init -a myorg/myapp',
|
||||
'$ balena device init --fleet MyFleet',
|
||||
'$ balena device init -f myorg/myfleet',
|
||||
];
|
||||
|
||||
public static usage = 'device init';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
}),
|
||||
fleet: cf.fleet,
|
||||
yes: cf.yes,
|
||||
advanced: flags.boolean({
|
||||
char: 'v',
|
||||
@ -95,15 +105,20 @@ export default class DeviceInitCmd extends Command {
|
||||
const logger = await Command.getLogger();
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
options.application ||= options.app || options.fleet;
|
||||
delete options.app;
|
||||
|
||||
// Get application and
|
||||
const application = (await getApplication(
|
||||
balena,
|
||||
options['application'] ||
|
||||
(await (await import('../../utils/patterns')).selectApplication()).id,
|
||||
(
|
||||
await (await import('../../utils/patterns')).selectApplication()
|
||||
).id,
|
||||
{
|
||||
$expand: {
|
||||
is_for__device_type: {
|
||||
|
@ -15,22 +15,36 @@
|
||||
* 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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
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 +54,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 +66,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 +82,8 @@ 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,
|
||||
...(isV13() ? {} : { app: cf.app, application: cf.application }),
|
||||
fleet: cf.fleet,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -81,13 +94,15 @@ export default class DeviceMoveCmd extends Command {
|
||||
DeviceMoveCmd,
|
||||
);
|
||||
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const { expandForAppName } = await import('../../utils/helpers');
|
||||
|
||||
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
|
||||
@ -98,15 +113,16 @@ export default class DeviceMoveCmd extends Command {
|
||||
const devices = await Promise.all(
|
||||
deviceIds.map(
|
||||
(uuid) =>
|
||||
balena.models.device.get(uuid, expandForAppName) as Promise<
|
||||
ExtendedDevice
|
||||
>,
|
||||
balena.models.device.get(
|
||||
uuid,
|
||||
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';
|
||||
@ -124,9 +140,7 @@ export default class DeviceMoveCmd extends Command {
|
||||
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;
|
||||
@ -138,43 +152,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,
|
||||
) &&
|
||||
// @ts-ignore using the extended device object prop
|
||||
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?`,
|
||||
);
|
||||
|
@ -79,19 +79,15 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
const sdk = getBalenaSdk();
|
||||
|
||||
// Get device info
|
||||
const {
|
||||
uuid,
|
||||
is_of__device_type,
|
||||
os_version,
|
||||
os_variant,
|
||||
} = (await sdk.models.device.get(params.uuid, {
|
||||
$select: ['uuid', 'os_version', 'os_variant'],
|
||||
$expand: {
|
||||
is_of__device_type: {
|
||||
$select: 'slug',
|
||||
const { uuid, is_of__device_type, os_version, os_variant } =
|
||||
(await sdk.models.device.get(params.uuid, {
|
||||
$select: ['uuid', 'os_version', 'os_variant'],
|
||||
$expand: {
|
||||
is_of__device_type: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as DeviceWithDeviceType;
|
||||
})) as DeviceWithDeviceType;
|
||||
|
||||
// Get current device OS version
|
||||
const currentOsVersion = sdk.models.device.getOsVersion({
|
||||
|
@ -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,7 +72,7 @@ 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}`);
|
||||
|
@ -20,7 +20,15 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
appToFleetOutputMsg,
|
||||
jsonInfo,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
import type { Application } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
@ -32,17 +40,19 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
v13: 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,33 +60,51 @@ 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,
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'v13'],
|
||||
},
|
||||
app: { ...cf.app, exclusive: ['application', 'fleet', 'v13'] },
|
||||
}),
|
||||
fleet: { ...cf.fleet, exclusive: ['app', 'application'] },
|
||||
json: cf.json,
|
||||
help: cf.help,
|
||||
v13: cf.v13,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
protected useAppWord = false;
|
||||
protected hasWarned = false;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
||||
this.useAppWord = !options.fleet && !options.v13 && !isV13();
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if (
|
||||
(options.application || options.app) &&
|
||||
!options.json &&
|
||||
process.stderr.isTTY
|
||||
) {
|
||||
this.hasWarned = true;
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
let devices;
|
||||
|
||||
@ -96,7 +124,8 @@ export default class DevicesCmd extends Command {
|
||||
devices = devices.map(function (device) {
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
const belongsToApplication =
|
||||
device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]?.app_name || null;
|
||||
|
||||
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
|
||||
@ -105,28 +134,32 @@ export default class DevicesCmd extends Command {
|
||||
return device;
|
||||
});
|
||||
|
||||
const jName = this.useAppWord ? 'application_name' : 'fleet_name';
|
||||
const tName = this.useAppWord ? 'APPLICATION NAME' : 'FLEET';
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'application_name',
|
||||
options.json
|
||||
? `application_name => ${jName}`
|
||||
: `application_name => ${tName}`,
|
||||
'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,
|
||||
),
|
||||
);
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetOutputMsg));
|
||||
}
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2019 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,13 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import type * as SDK from 'balena-sdk';
|
||||
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';
|
||||
|
||||
interface FlagsDef {
|
||||
discontinued: boolean;
|
||||
@ -30,17 +30,24 @@ interface FlagsDef {
|
||||
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').
|
||||
|
||||
The --verbose option adds extra columns/fields to the output, including the
|
||||
"STATE" column whose values are one of 'new', 'released' or 'discontinued'.
|
||||
However, 'discontinued' device types are only listed if the '--discontinued'
|
||||
option is used.
|
||||
|
||||
${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
|
||||
@ -60,7 +67,9 @@ export default class DevicesSupportedCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
discontinued: flags.boolean({
|
||||
description: 'include "discontinued" device types',
|
||||
description: isV13()
|
||||
? 'No effect (DEPRECATED)'
|
||||
: 'include "discontinued" device types (DEPRECATED)',
|
||||
}),
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
@ -69,45 +78,71 @@ export default class DevicesSupportedCmd extends Command {
|
||||
}),
|
||||
verbose: flags.boolean({
|
||||
char: 'v',
|
||||
description:
|
||||
'add extra columns in the tabular output (ALIASES, ARCH, STATE)',
|
||||
description: isV13()
|
||||
? 'No effect (DEPRECATED)'
|
||||
: 'add extra columns in the tabular output (DEPRECATED)',
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
||||
const dts = await getBalenaSdk().models.config.getDeviceTypes();
|
||||
let deviceTypes: Array<Partial<SDK.DeviceTypeJson.DeviceType>> = dts.map(
|
||||
(d) => {
|
||||
if (d.aliases && d.aliases.length) {
|
||||
// remove aliases that are equal to the slug
|
||||
d.aliases = d.aliases.filter((alias: string) => alias !== d.slug);
|
||||
if (!options.json) {
|
||||
// stringify the aliases array with commas and spaces
|
||||
d.aliases = [d.aliases.join(', ')];
|
||||
}
|
||||
} else {
|
||||
// ensure it is always an array (for the benefit of JSON output)
|
||||
d.aliases = [];
|
||||
}
|
||||
return d;
|
||||
},
|
||||
);
|
||||
if (!options.discontinued) {
|
||||
deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED');
|
||||
}
|
||||
const fields = options.verbose
|
||||
? ['slug', 'aliases', 'arch', 'state', 'name']
|
||||
: ['slug', 'aliases', 'arch', 'name'];
|
||||
deviceTypes = _.sortBy(
|
||||
deviceTypes.map((d) => {
|
||||
const picked = _.pick(d, fields);
|
||||
// 'BETA' renamed to 'NEW'
|
||||
picked.state = picked.state === 'BETA' ? 'NEW' : picked.state;
|
||||
return picked;
|
||||
const [dts, configDTs] = await Promise.all([
|
||||
getBalenaSdk().models.deviceType.getAllSupported({
|
||||
$expand: { is_of__cpu_architecture: { $select: 'slug' } },
|
||||
$select: ['slug', 'name'],
|
||||
}),
|
||||
fields,
|
||||
);
|
||||
getBalenaSdk().models.config.getDeviceTypes(),
|
||||
]);
|
||||
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
|
||||
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
|
||||
const discontinuedDTs = isV13()
|
||||
? []
|
||||
: configDTs.filter((dt) => dt.state === 'DISCONTINUED');
|
||||
const discontinuedDTsBySlug = _.keyBy(discontinuedDTs, (dt) => dt.slug);
|
||||
// set of slugs from models.deviceType.getAllSupported() plus slugs of
|
||||
// discontinued device types as per models.config.getDeviceTypes()
|
||||
const slugsOfInterest = new Set([
|
||||
...Object.keys(dtsBySlug),
|
||||
...Object.keys(discontinuedDTsBySlug),
|
||||
]);
|
||||
interface DT {
|
||||
slug: string;
|
||||
aliases: string[];
|
||||
arch: string;
|
||||
state?: string; // to be removed in CLI v13
|
||||
name: string;
|
||||
}
|
||||
let deviceTypes: DT[] = [];
|
||||
for (const slug of slugsOfInterest) {
|
||||
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,
|
||||
);
|
||||
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',
|
||||
});
|
||||
}
|
||||
const fields =
|
||||
options.verbose && !isV13()
|
||||
? ['slug', 'aliases', 'arch', 'state', 'name']
|
||||
: ['slug', 'aliases', 'arch', 'name'];
|
||||
deviceTypes = _.sortBy(deviceTypes, fields);
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
} else {
|
||||
|
69
lib/commands/env/add.ts
vendored
69
lib/commands/env/add.ts
vendored
@ -21,10 +21,16 @@ import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string; // device UUID
|
||||
help: void;
|
||||
quiet: boolean;
|
||||
@ -38,18 +44,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 +66,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 +101,11 @@ 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'] },
|
||||
...(isV13()
|
||||
? {}
|
||||
: { application: { ...cf.application, exclusive: ['fleet', 'device'] } }),
|
||||
fleet: { ...cf.fleet, exclusive: ['application', 'device'] },
|
||||
device: { ...cf.device, exclusive: ['application', 'fleet'] },
|
||||
help: cf.help,
|
||||
quiet: cf.quiet,
|
||||
service: cf.service,
|
||||
@ -110,9 +117,13 @@ export default class EnvAddCmd extends Command {
|
||||
);
|
||||
const cmd = this;
|
||||
|
||||
if (options.application && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.fleet;
|
||||
if (!options.application && !options.device) {
|
||||
throw new ExpectedError(
|
||||
'Either the --application or the --device option must be specified',
|
||||
'Either the --fleet or the --device option must be specified',
|
||||
);
|
||||
}
|
||||
|
||||
@ -161,7 +172,7 @@ export default class EnvAddCmd extends Command {
|
||||
params.value,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, app: ${app}`);
|
||||
console.error(`${err.message}, fleet: ${app}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@ -183,7 +194,7 @@ 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,
|
||||
@ -201,7 +212,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 +227,7 @@ async function setServiceVars(
|
||||
sdk,
|
||||
uuid,
|
||||
['id'],
|
||||
['app_name'],
|
||||
['slug'],
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, device: ${uuid}`);
|
||||
@ -225,11 +236,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 +269,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-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.
|
||||
@ -21,17 +21,24 @@ import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
appToFleetOutputMsg,
|
||||
warnify,
|
||||
} from '../utils/messages';
|
||||
import { isV13 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
config: boolean;
|
||||
device?: string; // device UUID
|
||||
json: boolean;
|
||||
help: void;
|
||||
service?: string; // service name
|
||||
verbose: boolean;
|
||||
v13: boolean;
|
||||
}
|
||||
|
||||
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
|
||||
@ -56,20 +63,20 @@ interface ServiceEnvironmentVariableInfo
|
||||
|
||||
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 +90,22 @@ 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')}
|
||||
|
||||
${appToFleetOutputMsg.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',
|
||||
@ -113,34 +120,47 @@ export default class EnvsCmd extends Command {
|
||||
: {
|
||||
all: flags.boolean({
|
||||
default: false,
|
||||
description: stripIndent`
|
||||
No-op since balena CLI v12.0.0.`,
|
||||
description: 'No-op since balena CLI v12.0.0.',
|
||||
hidden: true,
|
||||
}),
|
||||
application: {
|
||||
exclusive: ['device', 'fleet', 'v13'],
|
||||
...cf.application,
|
||||
},
|
||||
}),
|
||||
|
||||
application: { exclusive: ['device'], ...cf.application },
|
||||
fleet: { exclusive: ['device', 'application'], ...cf.fleet },
|
||||
config: flags.boolean({
|
||||
default: false,
|
||||
char: 'c',
|
||||
description: 'show configuration variables only',
|
||||
exclusive: ['service'],
|
||||
}),
|
||||
device: { exclusive: ['application'], ...cf.device },
|
||||
device: { exclusive: ['fleet', 'application'], ...cf.device },
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
verbose: cf.verbose,
|
||||
service: { exclusive: ['config'], ...cf.service },
|
||||
v13: cf.v13,
|
||||
};
|
||||
|
||||
protected useAppWord = false;
|
||||
protected hasWarned = false;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
|
||||
this.useAppWord = !options.fleet && !options.v13 && !isV13();
|
||||
|
||||
const variables: EnvironmentVariableInfo[] = [];
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
if (options.application && !options.json && process.stderr.isTTY) {
|
||||
this.hasWarned = true;
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.fleet;
|
||||
if (!options.application && !options.device) {
|
||||
throw new ExpectedError('You must specify an application or device');
|
||||
throw new ExpectedError('Missing --fleet or --device option');
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
@ -154,11 +174,11 @@ export default class EnvsCmd extends Command {
|
||||
balena,
|
||||
options.device,
|
||||
['uuid'],
|
||||
['app_name'],
|
||||
['slug'],
|
||||
);
|
||||
fullUUID = device.uuid;
|
||||
if (app) {
|
||||
appNameOrSlug = app.app_name;
|
||||
appNameOrSlug = app.slug;
|
||||
}
|
||||
}
|
||||
if (appNameOrSlug && options.service) {
|
||||
@ -174,7 +194,7 @@ export default class EnvsCmd extends Command {
|
||||
const target =
|
||||
(options.service ? `service "${options.service}" of ` : '') +
|
||||
(options.application
|
||||
? `application "${options.application}"`
|
||||
? `fleet "${options.application}"`
|
||||
: `device "${options.device}"`);
|
||||
throw new ExpectedError(`No environment variables found for ${target}`);
|
||||
}
|
||||
@ -190,11 +210,20 @@ export default class EnvsCmd extends Command {
|
||||
|
||||
// Replace undefined app names with 'N/A' or null
|
||||
varArray = varArray.map((i: EnvironmentVariableInfo) => {
|
||||
i.appName = i.appName || (options.json ? null : 'N/A');
|
||||
if (i.appName) {
|
||||
// use slug in v13, app name in v12 for compatibility
|
||||
i.appName = isV13()
|
||||
? i.appName
|
||||
: i.appName.substring(i.appName.indexOf('/') + 1);
|
||||
} else {
|
||||
i.appName = options.json ? null : 'N/A';
|
||||
}
|
||||
return i;
|
||||
});
|
||||
|
||||
fields.push(options.json ? 'appName' : 'appName => APPLICATION');
|
||||
const jName = this.useAppWord ? 'appName' : 'fleetName';
|
||||
const tName = this.useAppWord ? 'APPLICATION' : 'FLEET';
|
||||
fields.push(options.json ? `appName => ${jName}` : `appName => ${tName}`);
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
@ -203,10 +232,13 @@ export default class EnvsCmd extends Command {
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
this.log(
|
||||
stringifyVarArray<SDK.EnvironmentVariableBase>(varArray, fields),
|
||||
);
|
||||
const { pickAndRename } = await import('../utils/helpers');
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetOutputMsg));
|
||||
}
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
@ -227,7 +259,7 @@ async function validateServiceName(
|
||||
});
|
||||
if (services.length === 0) {
|
||||
throw new ExpectedError(
|
||||
`Service "${serviceName}" not found for application "${appName}"`,
|
||||
`Service "${serviceName}" not found for fleet "${appName}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -344,33 +376,13 @@ function fillInInfoFields(
|
||||
envVar.serviceName = (envVar.service as SDK.Service[])[0]?.service_name;
|
||||
} else if ('service_install' in envVar) {
|
||||
// envVar is of type DeviceServiceEnvironmentVariableInfo
|
||||
envVar.serviceName = ((envVar.service_install as SDK.ServiceInstall[])[0]
|
||||
?.installs__service as SDK.Service[])[0]?.service_name;
|
||||
envVar.serviceName = (
|
||||
(envVar.service_install as SDK.ServiceInstall[])[0]
|
||||
?.installs__service as SDK.Service[]
|
||||
)[0]?.service_name;
|
||||
}
|
||||
envVar.appName = appNameOrSlug;
|
||||
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 2020 Balena Ltd.
|
||||
* 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.
|
||||
@ -15,13 +15,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module 'umount' {
|
||||
export const umount: (
|
||||
device: string,
|
||||
callback: (err?: Error, stdout?: any, stderr?: any) => void,
|
||||
) => void;
|
||||
export const isMounted: (
|
||||
device: string,
|
||||
callback: (err: Error | null, isMounted?: boolean) => void,
|
||||
) => void;
|
||||
}
|
||||
import { FleetCreateCmd } from '../app/create';
|
||||
|
||||
export default FleetCreateCmd;
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
* 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.
|
||||
@ -15,25 +15,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module 'resin-image-fs' {
|
||||
import Bluebird = require('bluebird');
|
||||
import { FleetCmd } from '../app';
|
||||
|
||||
export interface ImageDefinition {
|
||||
image: string;
|
||||
partition: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export function readFile(options: {}): Bluebird<string>;
|
||||
export function writeFile(
|
||||
definition: ImageDefinition,
|
||||
contents: string,
|
||||
): Bluebird<void>;
|
||||
export function copy(
|
||||
input: ImageDefinition,
|
||||
output: ImageDefinition,
|
||||
): Bluebird<void>;
|
||||
export function listDirectory(
|
||||
definition: ImageDefinition,
|
||||
): Bluebird<string[]>;
|
||||
}
|
||||
export default FleetCmd;
|
20
lib/commands/fleet/purge.ts
Normal file
20
lib/commands/fleet/purge.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
import { FleetPurgeCmd } from '../app/purge';
|
||||
|
||||
export default FleetPurgeCmd;
|
20
lib/commands/fleet/rename.ts
Normal file
20
lib/commands/fleet/rename.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
import { FleetRenameCmd } from '../app/rename';
|
||||
|
||||
export default FleetRenameCmd;
|
20
lib/commands/fleet/restart.ts
Normal file
20
lib/commands/fleet/restart.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
import { FleetRestartCmd } from '../app/restart';
|
||||
|
||||
export default FleetRestartCmd;
|
20
lib/commands/fleet/rm.ts
Normal file
20
lib/commands/fleet/rm.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
import { FleetRmCmd } from '../app/rm';
|
||||
|
||||
export default FleetRmCmd;
|
20
lib/commands/fleets.ts
Normal file
20
lib/commands/fleets.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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 { FleetsCmd } from './apps';
|
||||
|
||||
export default FleetsCmd;
|
@ -21,9 +21,11 @@ import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import { parseAsLocalHostnameOrIp } from '../utils/validation';
|
||||
import { isV13 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
pollInterval?: number;
|
||||
help?: void;
|
||||
}
|
||||
@ -34,22 +36,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 +59,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 +77,8 @@ export default class JoinCmd extends Command {
|
||||
public static usage = 'join [deviceIpOrHostname]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
...(isV13() ? {} : { application: cf.application }),
|
||||
fleet: cf.fleet,
|
||||
pollInterval: flags.integer({
|
||||
description: 'the interval in minutes to check for updates',
|
||||
char: 'i',
|
||||
@ -98,7 +101,7 @@ export default class JoinCmd extends Command {
|
||||
logger,
|
||||
sdk,
|
||||
params.deviceIpOrHostname,
|
||||
options.application,
|
||||
options.application || options.fleet,
|
||||
options.pollInterval,
|
||||
);
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { promisify } from 'util';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
@ -59,63 +60,43 @@ export default class LocalConfigureCmd extends Command {
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const path = await import('path');
|
||||
const umount = await import('umount');
|
||||
const umountAsync = promisify(umount.umount);
|
||||
const isMountedAsync = promisify(umount.isMounted);
|
||||
const reconfix = await import('reconfix');
|
||||
const denymount = promisify(await import('denymount'));
|
||||
const { denyMount, safeUmount } = await import('../../utils/umount');
|
||||
const Logger = await import('../../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
|
||||
const configurationSchema = await this.prepareConnectionFile(params.target);
|
||||
|
||||
if (await isMountedAsync(params.target)) {
|
||||
await umountAsync(params.target);
|
||||
}
|
||||
|
||||
const dmOpts: any = {};
|
||||
if (process.pkg) {
|
||||
// when running in a standalone pkg install, the 'denymount'
|
||||
// executable is placed on the same folder as process.execPath
|
||||
dmOpts.executablePath = path.join(
|
||||
path.dirname(process.execPath),
|
||||
'denymount',
|
||||
await denyMount(params.target, async () => {
|
||||
// TODO: safeUmount umounts drives like '/dev/sdc', but does not
|
||||
// umount image files like 'balena.img'
|
||||
await safeUmount(params.target);
|
||||
const config = await reconfix.readConfiguration(
|
||||
configurationSchema,
|
||||
params.target,
|
||||
);
|
||||
}
|
||||
|
||||
const dmHandler = (cb: () => void) =>
|
||||
reconfix
|
||||
.readConfiguration(configurationSchema, params.target)
|
||||
.then(async (config: any) => {
|
||||
logger.logDebug('Current config:');
|
||||
logger.logDebug(JSON.stringify(config));
|
||||
const answers = await this.getConfiguration(config);
|
||||
logger.logDebug('New config:');
|
||||
logger.logDebug(JSON.stringify(answers));
|
||||
|
||||
if (!answers.hostname) {
|
||||
await this.removeHostname(configurationSchema);
|
||||
}
|
||||
return await reconfix.writeConfiguration(
|
||||
configurationSchema,
|
||||
answers,
|
||||
params.target,
|
||||
);
|
||||
})
|
||||
.asCallback(cb);
|
||||
|
||||
await denymount(params.target, dmHandler, dmOpts);
|
||||
logger.logDebug('Current config:');
|
||||
logger.logDebug(JSON.stringify(config));
|
||||
const answers = await this.getConfiguration(config);
|
||||
logger.logDebug('New config:');
|
||||
logger.logDebug(JSON.stringify(answers));
|
||||
if (!answers.hostname) {
|
||||
await this.removeHostname(configurationSchema);
|
||||
}
|
||||
await reconfix.writeConfiguration(
|
||||
configurationSchema,
|
||||
answers,
|
||||
params.target,
|
||||
);
|
||||
});
|
||||
|
||||
console.log('Done!');
|
||||
}
|
||||
|
||||
readonly BOOT_PARTITION = 1;
|
||||
readonly CONNECTIONS_FOLDER = '/system-connections';
|
||||
|
||||
getConfigurationSchema(connectionFileName?: string) {
|
||||
getConfigurationSchema(bootPartition: number, connectionFileName?: string) {
|
||||
connectionFileName ??= 'resin-wifi';
|
||||
return {
|
||||
mapper: [
|
||||
@ -154,14 +135,14 @@ export default class LocalConfigureCmd extends Command {
|
||||
path: this.CONNECTIONS_FOLDER.slice(1),
|
||||
// Reconfix still uses the older resin-image-fs, so still needs an
|
||||
// object-based partition definition.
|
||||
partition: this.BOOT_PARTITION,
|
||||
partition: bootPartition,
|
||||
},
|
||||
},
|
||||
config_json: {
|
||||
type: 'json',
|
||||
location: {
|
||||
path: 'config.json',
|
||||
partition: this.BOOT_PARTITION,
|
||||
partition: bootPartition,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -253,12 +234,13 @@ export default class LocalConfigureCmd extends Command {
|
||||
*/
|
||||
async prepareConnectionFile(target: string) {
|
||||
const _ = await import('lodash');
|
||||
const imagefs = await import('resin-image-fs');
|
||||
const imagefs = await import('balena-image-fs');
|
||||
const helpers = await import('../../utils/helpers');
|
||||
|
||||
const files = await imagefs.listDirectory({
|
||||
image: target,
|
||||
partition: this.BOOT_PARTITION,
|
||||
path: this.CONNECTIONS_FOLDER,
|
||||
const bootPartition = await helpers.getBootPartition(target);
|
||||
|
||||
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
|
||||
});
|
||||
|
||||
let connectionFileName;
|
||||
@ -266,18 +248,18 @@ export default class LocalConfigureCmd extends Command {
|
||||
// The required file already exists, nothing to do
|
||||
} else if (_.includes(files, 'resin-sample.ignore')) {
|
||||
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||
await imagefs.copy(
|
||||
{
|
||||
image: target,
|
||||
partition: this.BOOT_PARTITION,
|
||||
path: `${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
||||
},
|
||||
{
|
||||
image: target,
|
||||
partition: this.BOOT_PARTITION,
|
||||
path: `${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
},
|
||||
);
|
||||
await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||
const readFileAsync = promisify(_fs.readFile);
|
||||
const writeFileAsync = promisify(_fs.writeFile);
|
||||
const contents = await readFileAsync(
|
||||
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
return await writeFileAsync(
|
||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
contents,
|
||||
);
|
||||
});
|
||||
} else if (_.includes(files, 'resin-sample')) {
|
||||
// Legacy mode, to be removed later
|
||||
// We return the file name override from this branch
|
||||
@ -289,16 +271,14 @@ export default class LocalConfigureCmd extends Command {
|
||||
connectionFileName = 'resin-sample';
|
||||
} else {
|
||||
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
||||
await imagefs.writeFile(
|
||||
{
|
||||
image: target,
|
||||
partition: this.BOOT_PARTITION,
|
||||
path: `${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
},
|
||||
this.CONNECTION_FILE,
|
||||
);
|
||||
await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||
return await promisify(_fs.writeFile)(
|
||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
this.CONNECTION_FILE,
|
||||
);
|
||||
});
|
||||
}
|
||||
return await this.getConfigurationSchema(connectionFileName);
|
||||
return await this.getConfigurationSchema(bootPartition, connectionFileName);
|
||||
}
|
||||
|
||||
async removeHostname(schema: any) {
|
||||
|
@ -16,16 +16,11 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
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 type * as SDK from 'etcher-sdk';
|
||||
import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
@ -75,28 +70,36 @@ export default class LocalFlashCmd extends Command {
|
||||
LocalFlashCmd,
|
||||
);
|
||||
|
||||
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||
if (process.platform === 'linux') {
|
||||
const { promisify } = await import('util');
|
||||
const { exec } = await import('child_process');
|
||||
const execAsync = promisify(exec);
|
||||
let distroVersion = '';
|
||||
try {
|
||||
const info = await execAsync('cat /proc/version');
|
||||
distroVersion = info.stdout.toLowerCase();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
if (distroVersion.includes('microsoft')) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
This command is known not to work on WSL. Please use a CLI release
|
||||
for Windows (not WSL), or balenaEtcher.`);
|
||||
}
|
||||
}
|
||||
|
||||
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 file = new sourceDestination.File(
|
||||
params.image,
|
||||
sourceDestination.File.OpenFlags.Read,
|
||||
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,
|
||||
});
|
||||
const source = await file.getInnerSource();
|
||||
|
||||
const visuals = getVisuals();
|
||||
@ -105,29 +108,37 @@ export default class LocalFlashCmd extends Command {
|
||||
verifying: new visuals.Progress('Validating'),
|
||||
};
|
||||
|
||||
await multiWrite.pipeSourceToDestinations(
|
||||
await multiWrite.pipeSourceToDestinations({
|
||||
source,
|
||||
[drive],
|
||||
(_, error) => {
|
||||
// onFail
|
||||
console.log(getChalk().red.bold(error.message));
|
||||
destinations: [drive],
|
||||
onFail: (_, error) => {
|
||||
console.error(getChalk().red.bold(error.message));
|
||||
if (error.message.includes('EACCES')) {
|
||||
console.error(
|
||||
getChalk().red.bold(
|
||||
'Try running this command with elevated privileges, with sudo or in a shell running with admininstrator privileges.',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
(progress: SDK.multiWrite.MultiDestinationProgress) => {
|
||||
// onProgress
|
||||
onProgress: (progress) => {
|
||||
progressBars[progress.type].update(progress);
|
||||
},
|
||||
true, // verify
|
||||
);
|
||||
verify: true,
|
||||
});
|
||||
}
|
||||
|
||||
async getDrive(options: {
|
||||
drive?: string;
|
||||
}): Promise<SDK.sourceDestination.BlockDevice> {
|
||||
async getDrive(options: { drive?: string }): Promise<BlockDevice> {
|
||||
const drive = options.drive || (await getVisuals().drive('Select a drive'));
|
||||
|
||||
const sdk = await import('etcher-sdk');
|
||||
|
||||
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter({
|
||||
includeSystemDrives: () => false,
|
||||
unmountOnSuccess: false,
|
||||
write: true,
|
||||
direct: true,
|
||||
});
|
||||
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||
await scanner.start();
|
||||
try {
|
||||
|
@ -17,20 +17,26 @@
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import { promisify } from 'util';
|
||||
import * as _ from 'lodash';
|
||||
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,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
const BOOT_PARTITION = 1;
|
||||
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;
|
||||
@ -66,8 +72,8 @@ 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,15 +81,15 @@ 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 may be 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
|
||||
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/nm-settings.html
|
||||
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
|
||||
|
||||
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
|
||||
|
||||
@ -98,11 +104,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 +126,29 @@ 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'] },
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'fleet', 'device'],
|
||||
},
|
||||
}),
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
},
|
||||
config: flags.string({
|
||||
description:
|
||||
'path to a pre-generated config.json file to be injected in the OS image',
|
||||
}),
|
||||
'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,7 +160,7 @@ 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: { exclusive: ['app', 'application', 'fleet'], ...cf.device },
|
||||
'device-api-key': flags.string({
|
||||
char: 'k',
|
||||
description:
|
||||
@ -149,7 +168,7 @@ export default class OsConfigureCmd extends Command {
|
||||
}),
|
||||
'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:
|
||||
@ -172,9 +191,10 @@ export default class OsConfigureCmd extends Command {
|
||||
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;
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
await validateOptions(options);
|
||||
|
||||
@ -279,17 +299,18 @@ export default class OsConfigureCmd extends Command {
|
||||
};
|
||||
}),
|
||||
);
|
||||
const imagefs = await import('resin-image-fs');
|
||||
|
||||
const bootPartition = await helpers.getBootPartition(params.image);
|
||||
|
||||
const imagefs = await import('balena-image-fs');
|
||||
|
||||
for (const { name, content } of files) {
|
||||
await imagefs.writeFile(
|
||||
{
|
||||
image,
|
||||
partition: BOOT_PARTITION,
|
||||
path: path.join(CONNECTIONS_FOLDER, name),
|
||||
},
|
||||
content,
|
||||
);
|
||||
await imagefs.interact(image, bootPartition, async (_fs) => {
|
||||
return await promisify(_fs.writeFile)(
|
||||
path.join(CONNECTIONS_FOLDER, name),
|
||||
content,
|
||||
);
|
||||
});
|
||||
console.info(`Copied system-connection file: ${name}`);
|
||||
}
|
||||
}
|
||||
@ -297,29 +318,16 @@ export default class OsConfigureCmd extends Command {
|
||||
}
|
||||
|
||||
async function validateOptions(options: FlagsDef) {
|
||||
if (process.platform === 'win32') {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Unsupported platform error: the 'balena os configure' command currently requires
|
||||
the Windows Subsystem for Linux in order to run on Windows. It was tested with
|
||||
the Ubuntu 18.04 distribution from the Microsoft Store. With WSL, a balena CLI
|
||||
release for Linux (rather than Windows) should be installed: for example, the
|
||||
standalone zip package for Linux. (It is possible to have both a Windows CLI
|
||||
release and a Linux CLI release installed simultaneously.) For more information
|
||||
on WSL and the balena CLI installation options, please check:
|
||||
- https://docs.microsoft.com/en-us/windows/wsl/about
|
||||
- https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
||||
`);
|
||||
}
|
||||
// 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) {
|
||||
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']) {
|
||||
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']) {
|
||||
@ -378,7 +386,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.application}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,11 @@ export default class OsDownloadCmd extends Command {
|
||||
|
||||
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;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
@ -61,12 +61,7 @@ export default class OsInitializeCmd extends Command {
|
||||
public static usage = 'os initialize <image>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
type: cf.deviceType,
|
||||
drive: cf.drive,
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
@ -79,8 +74,6 @@ export default class OsInitializeCmd extends Command {
|
||||
OsInitializeCmd,
|
||||
);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
const { getManifest, sudo } = await import('../../utils/helpers');
|
||||
|
||||
console.info(`Initializing device ${INIT_WARNING_MESSAGE}`);
|
||||
@ -99,9 +92,9 @@ export default class OsInitializeCmd extends Command {
|
||||
options.yes,
|
||||
`This will erase ${answers.drive}. Are you sure?`,
|
||||
`Going to erase ${answers.drive}.`,
|
||||
true,
|
||||
);
|
||||
await umountAsync(answers.drive);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
await safeUmount(answers.drive);
|
||||
}
|
||||
|
||||
await sudo([
|
||||
@ -113,22 +106,8 @@ export default class OsInitializeCmd extends Command {
|
||||
]);
|
||||
|
||||
if (answers.drive != null) {
|
||||
// TODO: balena local makes use of ejectAsync, see below
|
||||
// DO we need this / should we do that here?
|
||||
|
||||
// getDrive = (drive) ->
|
||||
// driveListAsync().then (drives) ->
|
||||
// selectedDrive = _.find(drives, device: drive)
|
||||
|
||||
// if not selectedDrive?
|
||||
// throw new Error("Drive not found: #{drive}")
|
||||
|
||||
// return selectedDrive
|
||||
// if (os.platform() is 'win32') and selectedDrive.mountpoint?
|
||||
// ejectAsync = Promise.promisify(require('removedrive').eject)
|
||||
// return ejectAsync(selectedDrive.mountpoint)
|
||||
|
||||
await umountAsync(answers.drive);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
await safeUmount(answers.drive);
|
||||
console.info(`You can safely remove ${answers.drive} now`);
|
||||
}
|
||||
}
|
||||
|
@ -55,10 +55,8 @@ export default class OsVersionsCmd extends Command {
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(OsVersionsCmd);
|
||||
|
||||
const {
|
||||
versions: vs,
|
||||
recommended,
|
||||
} = await getBalenaSdk().models.os.getSupportedVersions(params.type);
|
||||
const { versions: vs, recommended } =
|
||||
await getBalenaSdk().models.os.getSupportedVersions(params.type);
|
||||
|
||||
vs.forEach((v) => {
|
||||
console.log(`v${v}` + (v === recommended ? ' (recommended)' : ''));
|
||||
|
@ -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,
|
||||
@ -24,23 +24,24 @@ import {
|
||||
getVisuals,
|
||||
stripIndent,
|
||||
} from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} 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 { isV13 } from '../utils/version';
|
||||
|
||||
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,25 +57,34 @@ 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
|
||||
preloaded.
|
||||
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 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
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
Warning: "balena preload" requires Docker to be correctly installed in
|
||||
your shell environment. For more information (including Windows support)
|
||||
check: https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
||||
Note that the this command requires Docker to be installed, as further detailed
|
||||
in the balena CLI's installation instructions:
|
||||
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
||||
The \`--dockerHost\` and \`--dockerPort\` flags allow a remote Docker engine to
|
||||
be used, however the image file must be accessible to the remote Docker engine
|
||||
on the same path given on the command line. This is because Docker's bind mount
|
||||
feature is used to "share" the image with a container that performs the preload.
|
||||
`;
|
||||
|
||||
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',
|
||||
];
|
||||
|
||||
@ -89,13 +99,16 @@ 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,
|
||||
...(isV13() ? {} : { 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',
|
||||
}),
|
||||
@ -106,7 +119,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,
|
||||
@ -150,6 +163,11 @@ Can be repeated to add multiple certificates.\
|
||||
PreloadCmd,
|
||||
);
|
||||
|
||||
if (options.app && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.app ||= options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const balenaPreload = await import('balena-preload');
|
||||
const visuals = getVisuals();
|
||||
@ -160,6 +178,14 @@ Can be repeated to add multiple certificates.\
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
await fs.promises.access(params.image);
|
||||
const path = await import('path');
|
||||
if (path.extname(params.image) === '.zip') {
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: A zip file is only accepted for the Intel Edison device type.
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new ExpectedError(
|
||||
`The provided image path does not exist: ${params.image}`,
|
||||
@ -173,7 +199,7 @@ Can be repeated to add multiple certificates.\
|
||||
const { getApplication } = await import('../utils/sdk');
|
||||
const application = await getApplication(balena, options.app);
|
||||
if (!application) {
|
||||
throw new ExpectedError(`Application not found: ${options.app}`);
|
||||
throw new ExpectedError(`Fleet not found: ${options.app}`);
|
||||
}
|
||||
options.app = application.slug;
|
||||
}
|
||||
@ -222,7 +248,7 @@ Can be repeated to add multiple certificates.\
|
||||
|
||||
if (dontCheckArch && !appId) {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
@ -299,7 +325,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'],
|
||||
@ -313,77 +338,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.getAll({
|
||||
$select: ['id', 'app_name', '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: 'app_name asc',
|
||||
})) as Array<
|
||||
ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
async selectApplication(deviceTypeSlug: string) {
|
||||
@ -400,11 +423,11 @@ 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,
|
||||
@ -415,7 +438,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(
|
||||
@ -448,23 +471,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,
|
||||
@ -514,7 +537,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)
|
||||
|
@ -36,7 +36,7 @@ enum BuildTarget {
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
applicationOrDevice: string;
|
||||
fleetOrDevice: string;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
@ -47,8 +47,8 @@ interface FlagsDef {
|
||||
pull: boolean;
|
||||
'noparent-check': boolean;
|
||||
'registry-secrets'?: string;
|
||||
gitignore?: boolean;
|
||||
nogitignore?: boolean;
|
||||
gitignore?: boolean; // v13: delete this flag
|
||||
nogitignore?: boolean; // v13: delete this flag
|
||||
nolive: boolean;
|
||||
detached: boolean;
|
||||
service?: string[];
|
||||
@ -58,14 +58,15 @@ interface FlagsDef {
|
||||
'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 +93,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 +116,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 +189,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,
|
||||
}),
|
||||
@ -236,11 +237,20 @@ export default class PushCmd extends Command {
|
||||
'Have each service use its own .dockerignore file. See "balena help push".',
|
||||
char: 'm',
|
||||
default: false,
|
||||
exclusive: ['gitignore'],
|
||||
exclusive: ['gitignore'], // v13: delete this line
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
gitignore: flags.boolean({
|
||||
description: stripIndent`
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is
|
||||
required until your project can be adapted.`,
|
||||
char: 'g',
|
||||
default: false,
|
||||
exclusive: ['multi-dockerignore'],
|
||||
}),
|
||||
nogitignore: flags.boolean({
|
||||
description:
|
||||
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
|
||||
@ -249,24 +259,23 @@ export default class PushCmd extends Command {
|
||||
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 +301,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 +315,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 +371,14 @@ 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,
|
||||
nogitignore: !options.gitignore, // v13: delete this line
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
@ -398,11 +404,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 +422,7 @@ export default class PushCmd extends Command {
|
||||
multiDockerignore: options['multi-dockerignore'],
|
||||
nocache: options.nocache,
|
||||
pull: options.pull,
|
||||
nogitignore: !options.gitignore,
|
||||
nogitignore: !options.gitignore, // v13: delete this line
|
||||
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`);
|
||||
}
|
||||
}
|
128
lib/commands/release/index.ts
Normal file
128
lib/commands/release/index.ts
Normal file
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @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';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import jsyaml = require('js-yaml');
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
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',
|
||||
}),
|
||||
};
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
if (options.composition) {
|
||||
await this.showComposition(params.commitOrId, balena);
|
||||
} else {
|
||||
await this.showReleaseInfo(params.commitOrId, balena);
|
||||
}
|
||||
}
|
||||
|
||||
async showComposition(
|
||||
commitOrId: string | number,
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
) {
|
||||
const release = await balena.models.release.get(commitOrId, {
|
||||
$select: 'composition',
|
||||
});
|
||||
|
||||
console.log(jsyaml.dump(release.composition));
|
||||
}
|
||||
|
||||
async showReleaseInfo(
|
||||
commitOrId: string | number,
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
) {
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
'commit',
|
||||
'created_at',
|
||||
'status',
|
||||
'semver',
|
||||
'is_final',
|
||||
'build_log',
|
||||
'start_timestamp',
|
||||
'end_timestamp',
|
||||
];
|
||||
|
||||
const release = await balena.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');
|
||||
|
||||
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']));
|
||||
}
|
||||
}
|
86
lib/commands/releases.ts
Normal file
86
lib/commands/releases.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, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationNameNote } from '../utils/messages';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
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> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'fleet',
|
||||
description: 'fleet name or slug',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
|
||||
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
'commit',
|
||||
'created_at',
|
||||
'status',
|
||||
'semver',
|
||||
'is_final',
|
||||
];
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const releases = await balena.models.release.getAllByApplication(
|
||||
params.fleet,
|
||||
{ $select: fields },
|
||||
);
|
||||
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
* Copyright 2017-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.
|
||||
@ -87,18 +87,17 @@ export default class ScanCmd extends Command {
|
||||
const ux = getCliUx();
|
||||
ux.action.start('Scanning for local balenaOS devices');
|
||||
|
||||
const localDevices: LocalBalenaOsDevice[] = await discover.discoverLocalBalenaOsDevices(
|
||||
discoverTimeout,
|
||||
);
|
||||
const localDevices: LocalBalenaOsDevice[] =
|
||||
await discover.discoverLocalBalenaOsDevices(discoverTimeout);
|
||||
const engineReachableDevices: boolean[] = await Promise.all(
|
||||
localDevices.map(async ({ address }: { address: string }) => {
|
||||
const docker = dockerUtils.createClient({
|
||||
const docker = await dockerUtils.createClient({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
}) as any;
|
||||
});
|
||||
try {
|
||||
await docker.pingAsync();
|
||||
await docker.ping();
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
@ -132,14 +131,14 @@ export default class ScanCmd extends Command {
|
||||
// Query devices for info
|
||||
const devicesInfo = await Promise.all(
|
||||
developmentDevices.map(async ({ host, address }) => {
|
||||
const docker = dockerUtils.createClient({
|
||||
const docker = await dockerUtils.createClient({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
}) as any;
|
||||
});
|
||||
const [dockerInfo, dockerVersion] = await Promise.all([
|
||||
docker.infoAsync().catchReturn('Could not get Docker info'),
|
||||
docker.versionAsync().catchReturn('Could not get Docker version'),
|
||||
docker.info(),
|
||||
docker.version(),
|
||||
]);
|
||||
return {
|
||||
host,
|
||||
@ -165,7 +164,13 @@ export default class ScanCmd extends Command {
|
||||
});
|
||||
}
|
||||
|
||||
const cmdOutput = productionDevicesInfo.concat(devicesInfo);
|
||||
const cmdOutput: Array<{
|
||||
host: string;
|
||||
address: string;
|
||||
osVariant: string;
|
||||
dockerInfo: any;
|
||||
dockerVersion: import('dockerode').DockerVersion | undefined;
|
||||
}> = [...productionDevicesInfo, ...devicesInfo];
|
||||
|
||||
// Output results
|
||||
if (!options.json && cmdOutput.length === 0) {
|
||||
|
@ -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({
|
||||
@ -124,10 +124,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,
|
||||
@ -136,7 +136,7 @@ export default class SshCmd extends Command {
|
||||
}
|
||||
|
||||
// Remote connection
|
||||
const { getProxyConfig, which } = await import('../utils/helpers');
|
||||
const { getProxyConfig } = await import('../utils/helpers');
|
||||
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
|
||||
const sdk = getBalenaSdk();
|
||||
|
||||
@ -147,7 +147,7 @@ export default class SshCmd extends Command {
|
||||
await Command.checkLoggedIn();
|
||||
const deviceUuid = await getOnlineTargetDeviceUuid(
|
||||
sdk,
|
||||
params.applicationOrDevice,
|
||||
params.fleetOrDevice,
|
||||
);
|
||||
|
||||
const device = await sdk.models.device.get(deviceUuid, {
|
||||
@ -156,6 +156,7 @@ export default class SshCmd extends Command {
|
||||
|
||||
const deviceId = device.id;
|
||||
const supervisorVersion = device.supervisor_version;
|
||||
const { which } = await import('../utils/which');
|
||||
|
||||
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
|
||||
useProxy ? which('proxytunnel', false) : undefined,
|
||||
@ -301,7 +302,7 @@ export default class SshCmd extends Command {
|
||||
// container
|
||||
const childProcess = await import('child_process');
|
||||
const { escapeRegExp } = await import('lodash');
|
||||
const { which } = await import('../utils/helpers');
|
||||
const { which } = await import('../utils/which');
|
||||
const { deviceContainerEngineBinary } = await import(
|
||||
'../utils/device/ssh'
|
||||
);
|
||||
|
@ -20,10 +20,16 @@ import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../utils/messages';
|
||||
import { isV13 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
duration?: string;
|
||||
help: void;
|
||||
@ -35,16 +41,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 +58,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 +77,11 @@ export default class SupportCmd extends Command {
|
||||
description: 'comma-separated list (no spaces) of device UUIDs',
|
||||
char: 'd',
|
||||
}),
|
||||
application: {
|
||||
...cf.application,
|
||||
...(isV13() ? {} : { 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 org/name slugs',
|
||||
},
|
||||
duration: flags.string({
|
||||
description:
|
||||
@ -91,6 +98,11 @@ export default class SupportCmd extends Command {
|
||||
SupportCmd,
|
||||
);
|
||||
|
||||
if (options.application && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
@ -98,9 +110,7 @@ export default class SupportCmd extends Command {
|
||||
|
||||
// Validation
|
||||
if (!options.device && !options.application) {
|
||||
throw new ExpectedError(
|
||||
'At least one device or application must be specified',
|
||||
);
|
||||
throw new ExpectedError('At least one device or fleet must be specified');
|
||||
}
|
||||
|
||||
if (options.duration != null && !enabling) {
|
||||
@ -135,10 +145,10 @@ export default class SupportCmd extends Command {
|
||||
// Process applications
|
||||
for (const appName of appNames) {
|
||||
if (enabling) {
|
||||
ux.action.start(`${enablingMessage} application ${appName}`);
|
||||
ux.action.start(`${enablingMessage} fleet ${appName}`);
|
||||
await balena.models.application.grantSupportAccess(appName, expiryTs);
|
||||
} else if (params.action === 'disable') {
|
||||
ux.action.start(`${disablingMessage} application ${appName}`);
|
||||
ux.action.start(`${disablingMessage} fleet ${appName}`);
|
||||
await balena.models.application.revokeSupportAccess(appName);
|
||||
}
|
||||
ux.action.stop();
|
||||
|
@ -19,14 +19,20 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
app?: string;
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -35,16 +41,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 +67,29 @@ 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'],
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device', 'release'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'fleet', 'device', 'release'],
|
||||
},
|
||||
}),
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['app', 'application', 'device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
exclusive: ['app', 'application', 'fleet', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
exclusive: ['app', 'application', 'fleet', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
@ -87,9 +101,10 @@ export default class TagRmCmd extends Command {
|
||||
TagRmCmd,
|
||||
);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
@ -130,9 +145,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:
|
||||
|
||||
|
@ -19,14 +19,20 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
app?: string;
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -36,9 +42,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 +54,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 +80,29 @@ 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'],
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device', 'release'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'fleet', 'device', 'release'],
|
||||
},
|
||||
}),
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['app', 'application', 'device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
exclusive: ['app', 'application', 'fleet', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
exclusive: ['app', 'application', 'fleet', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
@ -100,9 +114,10 @@ export default class TagSetCmd extends Command {
|
||||
TagSetCmd,
|
||||
);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
@ -151,9 +166,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:
|
||||
|
||||
|
@ -20,29 +20,34 @@ import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
appToFleetFlagMsg,
|
||||
warnify,
|
||||
} from '../utils/messages';
|
||||
import { isV13 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
app?: string;
|
||||
application?: string;
|
||||
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,21 +56,29 @@ 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'],
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'fleet', 'device', 'release'],
|
||||
},
|
||||
app: {
|
||||
...cf.app,
|
||||
exclusive: ['application', 'fleet', 'device', 'release'],
|
||||
},
|
||||
}),
|
||||
fleet: {
|
||||
...cf.fleet,
|
||||
exclusive: ['app', 'application', 'device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
exclusive: ['app', 'application', 'fleet', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
exclusive: ['app', 'application', 'fleet', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
@ -75,9 +88,10 @@ 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;
|
||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
||||
console.error(warnify(appToFleetFlagMsg));
|
||||
}
|
||||
options.application ||= options.app || options.fleet;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
@ -123,7 +137,7 @@ export default class TagsCmd extends Command {
|
||||
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}...`);
|
||||
|
||||
|
@ -43,7 +43,9 @@ export default class UtilAvailableDrivesCmd extends Command {
|
||||
|
||||
const sdk = await import('etcher-sdk');
|
||||
|
||||
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter({
|
||||
includeSystemDrives: () => false,
|
||||
});
|
||||
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||
await scanner.start();
|
||||
|
||||
|
230
lib/deprecation.ts
Normal file
230
lib/deprecation.ts
Normal file
@ -0,0 +1,230 @@
|
||||
/**
|
||||
* @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 });
|
||||
} 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.`;
|
||||
}
|
||||
}
|
@ -174,6 +174,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 +286,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,32 +14,22 @@
|
||||
* 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',
|
||||
});
|
||||
});
|
||||
|
||||
interface CachedUsername {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,43 @@ 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 });
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Event tracking error: ${e.message || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
119
lib/fast-boot.ts
Normal file
119
lib/fast-boot.ts
Normal file
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This module sets up the `fast-boot2` module, including testing whether
|
||||
* we have permissions over the cache file before even attempting to load
|
||||
* fast boot.
|
||||
* DON'T IMPORT BALENA-CLI MODULES HERE, as this module is loaded directly
|
||||
* from `bin/balena`, before the CLI's entrypoint in `lib/app.ts`.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
let fastBootStarted = false;
|
||||
|
||||
export async function start() {
|
||||
if (fastBootStarted) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await $start();
|
||||
fastBootStarted = true;
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`\
|
||||
[debug] Unable to start 'fast-boot2':
|
||||
[debug] ${(e.message || '').split('\n').join('\n[debug] ')}
|
||||
[debug] The CLI should still work, but it will run a bit slower.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stop() {
|
||||
if (fastBootStarted) {
|
||||
require('fast-boot2').stop();
|
||||
}
|
||||
fastBootStarted = false;
|
||||
}
|
||||
|
||||
async function $start() {
|
||||
const dotBalena = process.platform === 'win32' ? '_balena' : '.balena';
|
||||
// TODO: take into account `~/.balenarc.yml` or `./balenarc.yml`,
|
||||
// without hurting performance at this early loading stage.
|
||||
const dataDir = path.normalize(
|
||||
process.env.BALENARC_DATA_DIRECTORY || path.join(os.homedir(), dotBalena),
|
||||
);
|
||||
// Consider that the CLI may be installed to a folder owned by root
|
||||
// such as `/usr[/local]/lib/balena-cli`, while being executed by
|
||||
// a regular user account.
|
||||
const cacheFile = path.join(dataDir, 'cli-module-cache.json');
|
||||
const root = path.join(__dirname, '..');
|
||||
const [, pJson, pStat, nStat] = await Promise.all([
|
||||
ensureCanWrite(dataDir, cacheFile),
|
||||
import('../package.json'),
|
||||
stat(path.join(root, 'package.json'), { bigint: true }),
|
||||
stat(path.join(root, 'npm-shrinkwrap.json'), { bigint: true }),
|
||||
]);
|
||||
// Include timestamps to account for dev-time changes to node_modules
|
||||
const cacheKiller = `${pJson.version}-${pStat.mtimeMs}-${nStat.mtimeMs}`;
|
||||
require('fast-boot2').start({
|
||||
cacheFile,
|
||||
cacheKiller,
|
||||
cacheScope: root,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that `file` has write permission. If so, return straight away.
|
||||
* Throw an error if:
|
||||
* - `file` exists but does have write permissions.
|
||||
* - `file` does not exist and `dir` exists, but `dir` does not have
|
||||
* write permissions.
|
||||
* - `file` does not exist and `dir` does not exist, and an attempt
|
||||
* to create `dir` failed.
|
||||
*/
|
||||
async function ensureCanWrite(dir: string, file: string) {
|
||||
const { access, mkdir } = fs.promises;
|
||||
try {
|
||||
try {
|
||||
await access(file, fs.constants.W_OK);
|
||||
return;
|
||||
} catch (e) {
|
||||
// OK if file does not exist
|
||||
if (e.code !== 'ENOENT') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// file does not exist; ensure that the directory is writable
|
||||
await mkdir(dir, { recursive: true, mode: 0o755 });
|
||||
await access(dir, fs.constants.W_OK);
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to write file "${file}":\n${e.message}`);
|
||||
}
|
||||
}
|
141
lib/help.ts
141
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,69 +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));
|
||||
} else {
|
||||
console.log(
|
||||
`\n${bold('...MORE')} run ${cmd(
|
||||
'balena help --verbose',
|
||||
)} to list additional commands.`,
|
||||
// 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 {
|
||||
const cmd = cyan.bold('balena help --verbose');
|
||||
additionalCmdSection = [
|
||||
`\n${bold('...MORE')} run ${cmd} to list additional commands.`,
|
||||
];
|
||||
}
|
||||
|
||||
console.log(bold('\nGLOBAL OPTIONS'));
|
||||
console.log(' --help, -h');
|
||||
console.log(' --debug\n');
|
||||
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 {
|
||||
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,
|
||||
} = require('./utils/messages') as typeof import('./utils/messages');
|
||||
console.log(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)}`),
|
||||
);
|
||||
}
|
||||
return outLines.join('\n');
|
||||
}
|
||||
|
||||
protected formatCommands(commands: any[]): string {
|
||||
@ -184,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,10 @@ 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);
|
||||
}
|
||||
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
|
||||
}
|
||||
|
||||
// Enable bluebird long stack traces when in debug mode, must be set
|
||||
@ -87,11 +86,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 +111,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`.';
|
||||
|
@ -25,7 +25,8 @@
|
||||
export class CliSettings {
|
||||
public readonly settings: any;
|
||||
constructor() {
|
||||
this.settings = require('balena-settings-client') as typeof import('balena-settings-client');
|
||||
this.settings =
|
||||
require('balena-settings-client') as typeof import('balena-settings-client');
|
||||
}
|
||||
|
||||
public get<T>(name: string): T {
|
||||
|
@ -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];
|
||||
@ -139,6 +139,14 @@ export async function downloadOSImage(
|
||||
|
||||
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);
|
||||
|
||||
@ -188,20 +196,21 @@ async function resolveOSVersion(deviceType: string, version: string) {
|
||||
return version;
|
||||
}
|
||||
|
||||
const {
|
||||
versions: vs,
|
||||
recommended,
|
||||
} = await getBalenaSdk().models.os.getSupportedVersions(deviceType);
|
||||
const vs = (
|
||||
(await getBalenaSdk().models.hostapp.getAllOsVersions([deviceType]))[
|
||||
deviceType
|
||||
] ?? []
|
||||
).filter((v) => v.osType === 'default');
|
||||
|
||||
const choices = vs.map((v) => ({
|
||||
value: v,
|
||||
name: `v${v}` + (v === recommended ? ' (recommended)' : ''),
|
||||
value: v.rawVersion,
|
||||
name: `v${v.rawVersion}` + (v.isRecommended ? ' (recommended)' : ''),
|
||||
}));
|
||||
|
||||
return getCliForm().ask({
|
||||
message: 'Select the OS version:',
|
||||
type: 'list',
|
||||
choices,
|
||||
default: recommended,
|
||||
default: (vs.find((v) => v.isRecommended) ?? vs[0])?.rawVersion,
|
||||
});
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user