mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
209 Commits
v12.14.2
...
fix-window
Author | SHA1 | Date | |
---|---|---|---|
ae18df6710 | |||
8101ab38a6 | |||
0bae6546f2 | |||
40ab27df26 | |||
7d5a64f59a | |||
8115d156df | |||
08fc1a3924 | |||
950d173d27 | |||
ac49246141 | |||
0689074dd7 | |||
ee79c87723 | |||
9dc9556619 | |||
2f9212d622 | |||
2bf59530c4 | |||
a4fd7d6118 | |||
65f053dd6e | |||
8137b79078 | |||
e9b5773bcb | |||
4768f76385 | |||
d6b3249274 | |||
02a5466746 | |||
0831e5fa17 | |||
4681d901f8 | |||
6a55613199 | |||
893a39e891 | |||
fa4f91e08d | |||
54dc37dbd3 | |||
1b0c14feab | |||
20e0810d2a | |||
edc2e77ddd | |||
7da9a800cc | |||
2ba4405452 | |||
e7ebf1ad12 | |||
46249e319b | |||
fcd0932df8 | |||
34792ecce9 | |||
1e18096873 | |||
4da1ed3a56 | |||
92b8741288 | |||
6b4c28a026 | |||
849fc24158 | |||
16efb9748f | |||
9d177609f5 | |||
826b0659d6 | |||
46d7d1d068 | |||
47fcffe368 | |||
bb7cd7ac62 | |||
a83f6c95df | |||
7f000ee8c3 | |||
e5e7bb4757 | |||
37e6bd4b5c | |||
c48564e85a | |||
8460dac066 | |||
64ffcfdd91 | |||
077e25ebc4 | |||
709f009f9b | |||
116ab1fbc1 | |||
260a30532a | |||
7534042519 | |||
6b208ec2ab | |||
099d755900 | |||
3199f15662 | |||
4c8dc29946 | |||
2b22fb89f1 | |||
cf7d9246e5 | |||
0d3106af0e | |||
478b5dd363 | |||
0708608c7e | |||
c245dc70c2 | |||
4373ba7a5d | |||
2cc8d15c05 | |||
592efd0a2e | |||
31123d28f0 | |||
9b6ffecaba | |||
d0e4fa0e59 | |||
cf376316bc | |||
8f0f3bda29 | |||
c33409adb0 | |||
873eb1fc59 | |||
af70f16a9b | |||
e8d757ca28 | |||
63d3402924 | |||
8a506bc4c0 | |||
a14d89fe10 | |||
29ed0a232d | |||
8978221866 | |||
2974c203b5 | |||
c85acbd90b | |||
8a808e25d0 | |||
75687f51ac | |||
eddbdfe0dc | |||
d8acc3f814 | |||
fc8be3d8dc | |||
0ee02a4d73 | |||
568fcb9759 | |||
6133bb2096 | |||
48076464da | |||
1acf342fb0 | |||
340ca6577b | |||
0a8b3ce4e4 | |||
65c01ac172 | |||
4c9a22aba7 | |||
889fafcffc | |||
719cc2e4c9 | |||
e484701276 | |||
b1897a512d | |||
f98c25eaee | |||
b9c3b57b85 | |||
8aff330516 | |||
abdaf0043f | |||
960cb3098d | |||
e907f12445 | |||
799e0f9dea | |||
c389f41006 | |||
74ca5207ad | |||
3706db2436 | |||
6ec0b4a3bd | |||
e65caed64e | |||
b180eb7b73 | |||
9805854eab | |||
00c956394d | |||
b3510f205f | |||
e755d9f03f | |||
f9224b05af | |||
ece4d88bfd | |||
0dd7c33237 | |||
cd20f1765e | |||
0ca1faba09 | |||
9f8569e33f | |||
d7007721a7 | |||
f9f1863fdb | |||
93e18bea27 | |||
73f49765ec | |||
3a508dc397 | |||
bd5bf0135a | |||
e0c65bdef8 | |||
b9d90b9e38 | |||
d910319ba5 | |||
5e5a2c1c85 | |||
238c371ade | |||
504877c232 | |||
bdcf58471f | |||
46b9c586a6 | |||
273ea5ce4d | |||
d56fec6e36 | |||
cd81ff005f | |||
dee216eeaa | |||
d1539f405a | |||
d131fb4fa8 | |||
a0380848a0 | |||
ffa8e245ba | |||
8631e22686 | |||
f0bd3a38db | |||
88569066b5 | |||
c20bbe658b | |||
ac0ce8f702 | |||
42c6e1010f | |||
1f4554abe8 | |||
4e457da5a9 | |||
2e1570149d | |||
c647989054 | |||
44bd667648 | |||
2d042ee116 | |||
787966a0b6 | |||
a59d85e833 | |||
d0616acf1b | |||
d21a18f353 | |||
7d3dbc2c0b | |||
529b98552c | |||
99a478ee39 | |||
fb879d3020 | |||
4fb4cce842 | |||
f772957d29 | |||
fd9520224c | |||
c1afaa6cf3 | |||
8cb413c1c9 | |||
e96fca551e | |||
edb3ea53fb | |||
358a909214 | |||
eb74ca631a | |||
64ebebb121 | |||
abc62404ab | |||
af1c4b0d03 | |||
830e1f801d | |||
59c398fbf0 | |||
d7f49d2442 | |||
34597f629d | |||
3fa7eec8a9 | |||
1ee12b70bc | |||
ca7b1ae084 | |||
936d3cb62a | |||
230677e5e8 | |||
025f817eb6 | |||
54cceb688f | |||
648a73fd91 | |||
3691ae148e | |||
4496bc88f5 | |||
afded27692 | |||
c1a5718364 | |||
e021ad9af6 | |||
5c8a5165e0 | |||
71ff73c641 | |||
c35472e94d | |||
511bb05cb9 | |||
53b2b54b23 | |||
e7f753007f | |||
0afaf8502f | |||
3272b55dd9 | |||
4c664167f6 |
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -10,6 +10,3 @@ doc/cli.markdown text eol=lf
|
||||
# crlf for the eol conversion test files
|
||||
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
|
||||
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf
|
||||
|
||||
# Prevent auto merging of the npm-shrinkwrap.json file: see notes in CONTRIBUTING.md
|
||||
/npm-shrinkwrap.json merge=binary
|
||||
|
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@ -11,8 +11,8 @@ community can both contribute and benefit from the answers.*
|
||||
*Please also check that this issue is not a duplicate. If there is another issue describing
|
||||
the same problem or feature please add comments to the existing issue.*
|
||||
|
||||
*Thank you for your time and effort creating the issue report, and helping us improve the
|
||||
balena CLI!*
|
||||
*Thank you for your time and effort creating the issue report, and helping us improve
|
||||
the balena CLI!*
|
||||
|
||||
---
|
||||
|
||||
|
6
.mocharc-standalone.js
Normal file
6
.mocharc-standalone.js
Normal file
@ -0,0 +1,6 @@
|
||||
const commonConfig = require('./.mocharc.js');
|
||||
|
||||
module.exports = {
|
||||
...commonConfig,
|
||||
spec: ['tests/auth/*.spec.ts', 'tests/commands/**/*.spec.ts'],
|
||||
};
|
8
.mocharc.js
Normal file
8
.mocharc.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
spec: 'tests/commands/app/create.spec.ts',
|
||||
reporter: 'spec',
|
||||
require: 'ts-node/register/transpile-only',
|
||||
file: './tests/config-tests',
|
||||
timeout: 12000,
|
||||
spec: 'tests/**/*.spec.ts',
|
||||
};
|
21
.resinci.yml
21
.resinci.yml
@ -2,30 +2,19 @@
|
||||
npm:
|
||||
platforms:
|
||||
- name: linux
|
||||
os: alpine
|
||||
os: ubuntu
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86
|
||||
node_versions:
|
||||
- "10"
|
||||
- name: darwin
|
||||
os: macos
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- name: windows
|
||||
os: windows
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- name: windows
|
||||
os: windows
|
||||
architecture: x86
|
||||
node_versions:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
|
||||
docker:
|
||||
publish: false
|
||||
|
File diff suppressed because it is too large
Load Diff
904
CHANGELOG.md
904
CHANGELOG.md
@ -4,6 +4,910 @@ 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.29.0 - 2020-12-01
|
||||
|
||||
* scan: Print production devices' info on scan [Marios Balamatsias]
|
||||
|
||||
## 12.28.3 - 2020-11-26
|
||||
|
||||
* Add ability to disable analytics for performance testing [Scott Lowe]
|
||||
|
||||
## 12.28.2 - 2020-11-20
|
||||
|
||||
* docs: Add references to the masterclasses in the CLI help and README [Paulo Castro]
|
||||
* Fix debug message logic (don't suggest --debug if it is already being used) [Paulo Castro]
|
||||
* Fix unhandled promise rejection when ~/.balena is not accessible [Paulo Castro]
|
||||
|
||||
## 12.28.1 - 2020-11-20
|
||||
|
||||
* scan: Prevent spinner animation output to stdout when --json is used [Paulo Castro]
|
||||
|
||||
## 12.28.0 - 2020-11-19
|
||||
|
||||
* push: Reduce memory usage when filtering files with dockerignore [Paulo Castro]
|
||||
* Livepush: Refactor dockerignore filtering and add test cases [Paulo Castro]
|
||||
* Livepush: Ignore paths set in .dockerignore files [Josh Bowling]
|
||||
|
||||
## 12.27.4 - 2020-11-15
|
||||
|
||||
* Test code optimization: avoid running ~70 test cases twice [Paulo Castro]
|
||||
* docs: Add note about macOS Big Sur notarization workaround [Paulo Castro]
|
||||
|
||||
## 12.27.3 - 2020-11-11
|
||||
|
||||
* Avoid reporting balenarc parsing errors [Scott Lowe]
|
||||
|
||||
## 12.27.2 - 2020-11-09
|
||||
|
||||
* Modify `os download` help to mention dev images [Thomas Manning]
|
||||
|
||||
## 12.27.1 - 2020-11-06
|
||||
|
||||
* Improve application-identifier disambiguation [Scott Lowe]
|
||||
|
||||
## 12.27.0 - 2020-11-05
|
||||
|
||||
* Add command app purge [Scott Lowe]
|
||||
|
||||
## 12.26.2 - 2020-11-05
|
||||
|
||||
* config generate + openBalena v3: Fix "Cannot read property '__id' of undefined" [Paulo Castro]
|
||||
|
||||
## 12.26.1 - 2020-10-31
|
||||
|
||||
* devices: Fix "TypeError: Cannot read property 'slug' of undefined" [Paulo Castro]
|
||||
|
||||
## 12.26.0 - 2020-10-29
|
||||
|
||||
* Add command device purge [Scott Lowe]
|
||||
|
||||
## 12.25.6 - 2020-10-28
|
||||
|
||||
* ssh: Fix "Found more than one container with a service name <name>" [Paulo Castro]
|
||||
|
||||
## 12.25.5 - 2020-10-27
|
||||
|
||||
* Remove need for hardcoded list of command ids [Scott Lowe]
|
||||
|
||||
## 12.25.4 - 2020-10-25
|
||||
|
||||
* Update Contributing document re commit messages / versionbot / changelog [Paulo Castro]
|
||||
* config generate: Fix "Application is ambiguous" when app slug is used [Paulo Castro]
|
||||
* config generate: Fix device type compatibility check [Paulo Castro]
|
||||
|
||||
## 12.25.3 - 2020-10-21
|
||||
|
||||
* build/deploy: Add more test cases (--buildArg option) [Paulo Castro]
|
||||
* Fix typing (don't assume that 'docker-toolbelt' uses Bluebird promises) [Paulo Castro]
|
||||
* build/deploy: Fix --buildArg option with docker-compose.yml projects [Paulo Castro]
|
||||
* build/deploy: Fix image size notice at end of build [Paulo Castro]
|
||||
* Convert more code to Typescript (compose.js) [Paulo Castro]
|
||||
|
||||
## 12.25.2 - 2020-10-21
|
||||
|
||||
* Revert styling of "balena CLI" as "balenaCLI" [Paulo Castro]
|
||||
* Add help note regarding the version of Node.js printed by `balena version -a` [Paulo Castro]
|
||||
* preload: Fix parsing of `--add-certificate` option, amend help for `--app` [Paulo Castro]
|
||||
|
||||
## 12.25.1 - 2020-10-13
|
||||
|
||||
* Treat authorization errors as expected [Scott Lowe]
|
||||
|
||||
## 12.25.0 - 2020-10-11
|
||||
|
||||
* Refactor initialization code (delete app-oclif.ts and app-common.ts) [Paulo Castro]
|
||||
* Support BALENARC_NO_SENTRY env var to disable Sentry.io error reporting [Paulo Castro]
|
||||
* Update Sentry package (may fix "Maximum call stack size exceeded") [Paulo Castro]
|
||||
|
||||
## 12.24.1 - 2020-10-07
|
||||
|
||||
* login: sign up at the configured balena instance [Matthew McGinn]
|
||||
|
||||
## 12.24.0 - 2020-10-06
|
||||
|
||||
* scan: Add '--json' option to help with scripting [Paulo Castro]
|
||||
|
||||
## 12.23.4 - 2020-10-05
|
||||
|
||||
* Update CONTRIBUTING.md re balena-dev workflow [Paulo Castro]
|
||||
* Add bin/balena* scripts to linter paths [Paulo Castro]
|
||||
* Workaround balena-dev/oclif compatibility issues [Scott Lowe]
|
||||
|
||||
## 12.23.3 - 2020-10-02
|
||||
|
||||
* Rename actions-oclif/ to commands/ [Scott Lowe]
|
||||
|
||||
## 12.23.2 - 2020-10-02
|
||||
|
||||
* push: Fix accidental rename of `-e` (emulated) option [Paulo Castro]
|
||||
|
||||
## 12.23.1 - 2020-09-28
|
||||
|
||||
* Update the CONTRIBUTING.md document [Paulo Castro]
|
||||
|
||||
## 12.23.0 - 2020-09-25
|
||||
|
||||
* Add new command `support` [Scott Lowe]
|
||||
|
||||
## 12.22.2 - 2020-09-19
|
||||
|
||||
* deploy: Fix unexpected exit with "Everything is up to date" [Paulo Castro]
|
||||
|
||||
## 12.22.1 - 2020-09-19
|
||||
|
||||
* Style "balena CLI" as "balenaCLI" and "balena cloud" as "balenaCloud" [Paulo Castro]
|
||||
* Reorganize and simplify installation instructions [Paulo Castro]
|
||||
|
||||
## 12.22.0 - 2020-09-18
|
||||
|
||||
* Add new command `device restart` [Scott Lowe]
|
||||
|
||||
## 12.21.1 - 2020-09-16
|
||||
|
||||
* scan: Fix "CLI could not be loaded" with the standalone zip installer [Paulo Castro]
|
||||
|
||||
## 12.21.0 - 2020-09-16
|
||||
|
||||
* Add new command `app rename` [Scott Lowe]
|
||||
|
||||
## 12.20.0 - 2020-09-10
|
||||
|
||||
* Minor fix to device rm [Scott Lowe]
|
||||
* Update registry secrets example URL [Scott Lowe]
|
||||
* Improve command suggestions, add topic help [Scott Lowe]
|
||||
|
||||
## 12.19.0 - 2020-09-10
|
||||
|
||||
* Fix numerical id support in device rm [Scott Lowe]
|
||||
* Fix numerical id support in device move [Scott Lowe]
|
||||
* Fix required status of param in os versions [Scott Lowe]
|
||||
* Add ability to specify poll interval in join command [Scott Lowe]
|
||||
* Improve error handling in internal scandevices [Scott Lowe]
|
||||
|
||||
## 12.18.0 - 2020-09-09
|
||||
|
||||
* push (local device): Add --pull option to force pulling base images again [Paulo Castro]
|
||||
* push: Reformat help output to fit in 80 characters [Paulo Castro]
|
||||
|
||||
## 12.17.2 - 2020-09-08
|
||||
|
||||
* Fix error displaying info for devices without commits [Scott Lowe]
|
||||
|
||||
## 12.17.1 - 2020-09-08
|
||||
|
||||
* Fix usage info for env rename [Scott Lowe]
|
||||
* Fix typo in docs help section [Scott Lowe]
|
||||
* Fix issue with replaced command checks [Scott Lowe]
|
||||
|
||||
## 12.17.0 - 2020-09-07
|
||||
|
||||
* Display command suggestions, when command not recognized [Scott Lowe]
|
||||
* Convert help to oclif, remove capitano [Scott Lowe]
|
||||
|
||||
## 12.16.0 - 2020-09-04
|
||||
|
||||
* build, deploy: Add --pull option to force pulling base images again [Paulo Castro]
|
||||
|
||||
## 12.15.1 - 2020-09-04
|
||||
|
||||
* Modify undervoltage status display in device command [Scott Lowe]
|
||||
|
||||
## 12.15.0 - 2020-09-03
|
||||
|
||||
* Add support for new device metrics to device command [Scott Lowe]
|
||||
|
||||
## 12.14.18 - 2020-08-28
|
||||
|
||||
* Convert oclif actions to async/await [Pagan Gazzard]
|
||||
|
||||
## 12.14.17 - 2020-08-27
|
||||
|
||||
* preload: Improve the typings [Thodoris Greasidis]
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to 15.3.7 [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-sdk-15.3.7 - 2020-08-27
|
||||
>
|
||||
> * Improve $orderby typing to allow `[{a: 'desc'}, {b: 'asc'}]` [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sdk-15.3.6 - 2020-08-26
|
||||
>
|
||||
> * Document how to use with pkg [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.3.5 - 2020-08-26
|
||||
>
|
||||
> * Use a more semantic parameter name for device.getDeviceSlug [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.3.4 - 2020-08-26
|
||||
>
|
||||
> * tests: Update the tests for the new maximum application name size [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.3.3 - 2020-08-21
|
||||
>
|
||||
> * typings: Fix nested $count support in the strict pine variant [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.3.2 - 2020-08-20
|
||||
>
|
||||
> * Update TypeScript to 4.0 [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.14.16 - 2020-08-27
|
||||
|
||||
* Update to typescript 4.0 [Pagan Gazzard]
|
||||
|
||||
## 12.14.15 - 2020-08-27
|
||||
|
||||
* Convert command preload to oclif, typescript [Scott Lowe]
|
||||
|
||||
## 12.14.14 - 2020-08-26
|
||||
|
||||
* Fix device UUID parsing for 'balena tunnel' [Alexis Svinartchouk]
|
||||
|
||||
## 12.14.13 - 2020-08-26
|
||||
|
||||
* preload: Fix splash image file name for balenaOS >= 2.53.0 [Paulo Castro]
|
||||
|
||||
## 12.14.12 - 2020-08-22
|
||||
|
||||
* Convert deploy command to oclif [Scott Lowe]
|
||||
|
||||
## 12.14.11 - 2020-08-21
|
||||
|
||||
* Shorten `env add` command summary to prevent wrapping [Scott Lowe]
|
||||
* Fix help template bug affecting discrete value options [Scott Lowe]
|
||||
|
||||
## 12.14.10 - 2020-08-14
|
||||
|
||||
* build: Fix --buildArg and --cache-from options (broken since v12.9.9) [Paulo Castro]
|
||||
|
||||
## 12.14.9 - 2020-08-14
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependencies [Pagan Gazzard] </summary>
|
||||
|
||||
> ### balena-sdk-15.3.1 - 2020-08-13
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Bump balena-request to fix breaking user's stored token on token refresh [Thodoris Greasidis] </summary>
|
||||
>
|
||||
>> #### balena-request-11.1.1 - 2020-08-13
|
||||
>>
|
||||
>> * Stop refreshing the token on absolute urls [Thodoris Greasidis]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.14.8 - 2020-08-13
|
||||
|
||||
* build / push: Fix sudden process crash (update tar-utils dependency) [Paulo Castro]
|
||||
|
||||
## 12.14.7 - 2020-08-13
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependencies [Pagan Gazzard] </summary>
|
||||
|
||||
> ### balena-sdk-15.3.0 - 2020-08-12
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update balena-pine and add custom typings for prepare/subscribe [Pagan Gazzard] </summary>
|
||||
>
|
||||
>> #### balena-pine-12.3.0 - 2020-08-12
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update pinejs-client-core to 6.6.1 [Pagan Gazzard] </summary>
|
||||
>>
|
||||
>>> ##### pinejs-client-js-6.6.1 - 2020-08-11
|
||||
>>>
|
||||
>>> * Fix typing when id is specified to be `AnyObject | undefined` [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### pinejs-client-js-6.6.0 - 2020-08-11
|
||||
>>>
|
||||
>>> * Deprecate `$expand: { 'a/$count': {...} }` [Pagan Gazzard]
|
||||
>>> * Deprecate `resource: 'a/$count'` and update typings to reflect it [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### pinejs-client-js-6.5.0 - 2020-08-11
|
||||
>>>
|
||||
>>> * Add `options: { $count: { ... } }` sugar for top level $count [Pagan Gazzard]
|
||||
>>> * Add `$expand: { a: { $count: { ... } } }` sugar for $count in expands [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### pinejs-client-js-6.4.0 - 2020-08-11
|
||||
>>>
|
||||
>>> * Improve return typing of `subscribe` method [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### pinejs-client-js-6.3.0 - 2020-08-11
|
||||
>>>
|
||||
>>> * Fix Poll.on typings [Pagan Gazzard]
|
||||
>>> * Improve return typing when id is passed to GET methods [Pagan Gazzard]
|
||||
>>> * Remove `PromiseResult` type, use `Promise<PromiseResultTypes>` instead [Pagan Gazzard]
|
||||
>>> * Remove `PromiseObj` type, use `Promise<{}>` instead [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### pinejs-client-js-6.2.0 - 2020-08-10
|
||||
>>>
|
||||
>>> * Add `$filter: { a: { $count: 1 } }` sugar for $count in filters [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### pinejs-client-js-6.1.2 - 2020-08-10
|
||||
>>>
|
||||
>>> * Remove redundant ParamsObj/SubscribeParamsObj types [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### pinejs-client-js-6.1.1 - 2020-08-10
|
||||
>>>
|
||||
>>> * Make use of `mapObj` helper in more places [Pagan Gazzard]
|
||||
>>> * Use `Object.keys` in preference to `hasOwnProperty` where applicable [Pagan Gazzard]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.14.6 - 2020-08-12
|
||||
|
||||
* Tests: check shrinkwrap is deduped [Pagan Gazzard]
|
||||
* Allow rebasing of npm-shrinkwrap [Pagan Gazzard]
|
||||
|
||||
## 12.14.5 - 2020-08-11
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to 15.x [Pagan Gazzard] </summary>
|
||||
|
||||
> ### balena-sdk-15.2.1 - 2020-08-03
|
||||
>
|
||||
> * Convert majority to async/await [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sdk-15.2.0 - 2020-07-31
|
||||
>
|
||||
> * device: add method to update target supervisor release [Matthew McGinn]
|
||||
>
|
||||
> ### balena-sdk-15.1.1 - 2020-07-27
|
||||
>
|
||||
> * Deduplicate device update methods [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sdk-15.1.0 - 2020-07-27
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update balena-pine to add support for and make use of named keys [Pagan Gazzard] </summary>
|
||||
>
|
||||
>> #### balena-pine-12.2.0 - 2020-07-22
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update pinejs-client-core [Pagan Gazzard] </summary>
|
||||
>>
|
||||
>>> ##### pinejs-client-js-6.1.0 - 2020-07-21
|
||||
>>>
|
||||
>>> * Add support for using named ids [Pagan Gazzard]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>>
|
||||
>> #### balena-request-11.1.0 - 2020-07-16
|
||||
>>
|
||||
>> * Add lazy loading for most modules [Pagan Gazzard]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-sdk-15.0.3 - 2020-07-27
|
||||
>
|
||||
> * typings: Fix the PineWithSelect & related type helpers [Thodoris Greasidis]
|
||||
> * typings: Use the native TypeScript Omit type helper [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.0.2 - 2020-07-22
|
||||
>
|
||||
> * Fix code snippet for initializing balena-sdk [Vipul Gupta (@vipulgupta2048)]
|
||||
>
|
||||
> ### balena-sdk-15.0.1 - 2020-07-15
|
||||
>
|
||||
> * Fix SupportTier/includes__SLA typing [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sdk-15.0.0 - 2020-07-15
|
||||
>
|
||||
> * **BREAKING** Export setSharedOptions & fromSharedOptions separately [Thodoris Greasidis]
|
||||
> * **BREAKING** Export as an ES6 module [Thodoris Greasidis]
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update dependencies and switch all returned promises to native promises [Pagan Gazzard] </summary>
|
||||
>
|
||||
>> #### balena-auth-4.0.2 - 2020-07-13
|
||||
>>
|
||||
>> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-auth-4.0.1 - 2020-07-03
|
||||
>>
|
||||
>> * Explicitly add tslib dependency [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-auth-4.0.0 - 2020-07-02
|
||||
>>
|
||||
>> * Update to balena-settings-storage 6.x [Pagan Gazzard]
|
||||
>> * Update target to es2015 [Pagan Gazzard]
|
||||
>> * Switch to native promises [Pagan Gazzard]
|
||||
>> * Enable strict type checking [Pagan Gazzard]
|
||||
>> * Specify node 10+ [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-auth-3.1.1 - 2020-07-02
|
||||
>>
|
||||
>> * Switch to @balena/lint for linting [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-pine-12.1.1 - 2020-07-13
|
||||
>>
|
||||
>> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-pine-12.1.0 - 2020-07-06
|
||||
>>
|
||||
>> * Update balena-auth to 4.x and balena-request to 11.x [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-pine-12.0.1 - 2020-07-03
|
||||
>>
|
||||
>> * Use typescript import helpers [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-pine-12.0.0 - 2020-06-26
|
||||
>>
|
||||
>> * Stop actively supporting node 8 [Thodoris Greasidis]
|
||||
>> * Convert to async await [Thodoris Greasidis]
|
||||
>> * Add balenaCI repo.yml [Thodoris Greasidis]
|
||||
>> * karma.conf.js: Combine declaration & assignment of karmaConfig [Thodoris Greasidis]
|
||||
>> * Bump @balena/lint to v5 [Thodoris Greasidis]
|
||||
>> * Drop getPine() in favor of an es6 export of the BalenaPine class [Thodoris Greasidis]
|
||||
>> * Drop the API_PREFIX property in favor of the apiPrefix [Thodoris Greasidis]
|
||||
>> * Bump to pinejs-client v6 which requires es2015 & drops Bluebird promises [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-pine-11.2.1 - 2020-06-15
|
||||
>>
|
||||
>> * Convert karma.conf to js [Thodoris Greasidis]
|
||||
>> * Bump balena-config-karma to v3 [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-register-device-7.1.0 - 2020-07-13
|
||||
>>
|
||||
>> * Switch from randomstring to uuid for generating device uuids [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-register-device-7.0.1 - 2020-07-13
|
||||
>>
|
||||
>> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-register-device-7.0.0 - 2020-07-06
|
||||
>>
|
||||
>> * Convert to type checked javascript [Pagan Gazzard]
|
||||
>> * Drop callback interface in favor of promise interface [Pagan Gazzard]
|
||||
>> * Switch to a named export [Pagan Gazzard]
|
||||
>> * Convert to typescript [Pagan Gazzard]
|
||||
>> * Update to typed-error 3.x [Pagan Gazzard]
|
||||
>> * Switch to returning native promises [Pagan Gazzard]
|
||||
>> * Update to balena-request 11.x [Pagan Gazzard]
|
||||
>> * Use typescript import helpers [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-register-device-6.1.6 - 2020-05-26
|
||||
>>
|
||||
>> * Export ApiError [Cameron Diver]
|
||||
>>
|
||||
>> #### balena-register-device-6.1.5 - 2020-05-21
|
||||
>>
|
||||
>> * Convert tests to js [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-register-device-6.1.4 - 2020-05-21
|
||||
>>
|
||||
>> * Install typed-error v2 [Cameron Diver]
|
||||
>>
|
||||
>> #### balena-register-device-6.1.3 - 2020-05-20
|
||||
>>
|
||||
>> * Extend API exception to include full response object [Miguel Casqueira]
|
||||
>>
|
||||
>> #### balena-register-device-6.1.2 - 2020-05-20
|
||||
>>
|
||||
>> * Update mocha to fix node v12 deprecation warning [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-request-11.0.4 - 2020-07-14
|
||||
>>
|
||||
>> * Fix body overwriting on nodejs [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-request-11.0.3 - 2020-07-13
|
||||
>>
|
||||
>> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-request-11.0.2 - 2020-07-06
|
||||
>>
|
||||
>> * Fix tslib dependency [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-request-11.0.1 - 2020-07-03
|
||||
>>
|
||||
>> * Fix passing baseUrl to refreshToken if the request uses an absolute url [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-request-11.0.0 - 2020-07-03
|
||||
>>
|
||||
>> * Convert to type checked javascript [Pagan Gazzard]
|
||||
>> * Switch to returning native promises [Pagan Gazzard]
|
||||
>> * Drop support for nodejs < 10 [Pagan Gazzard]
|
||||
>> * Update balena-auth to 4.x [Pagan Gazzard]
|
||||
>> * Remove rindle dependency [Pagan Gazzard]
|
||||
>> * Update fetch-ponyfill to 6.x [Pagan Gazzard]
|
||||
>> * Remove proxy tests as global-tunnel-ng only supports nodejs < 10.16.0 [Pagan Gazzard]
|
||||
>> * Switch to a named export [Pagan Gazzard]
|
||||
>> * Use typescript import helpers [Pagan Gazzard]
|
||||
>> * Bump balena-config-karma & convert karma.conf.coffee to js [Thodoris Greasidis]
|
||||
>> * Change the browser request timeout error to be consistent with node [Thodoris Greasidis]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
> * **BREAKING** billing: Make the organization parameter fist & required [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-config-json-4.1.0 - 2020-08-04
|
||||
>
|
||||
> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update dependencies [Pagan Gazzard] </summary>
|
||||
>
|
||||
>> #### balena-sdk-15.2.1 - 2020-08-03
|
||||
>>
|
||||
>> * Convert majority to async/await [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-15.2.0 - 2020-07-31
|
||||
>>
|
||||
>> * device: add method to update target supervisor release [Matthew McGinn]
|
||||
>>
|
||||
>> #### balena-sdk-15.1.1 - 2020-07-27
|
||||
>>
|
||||
>> * Deduplicate device update methods [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-15.1.0 - 2020-07-27
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update balena-pine to add support for and make use of named keys [Pagan Gazzard] </summary>
|
||||
>>
|
||||
>>> ##### balena-pine-12.2.0 - 2020-07-22
|
||||
>>>
|
||||
>>>
|
||||
>>> <details>
|
||||
>>> <summary> Update pinejs-client-core [Pagan Gazzard] </summary>
|
||||
>>>
|
||||
>>>> ###### pinejs-client-js-6.1.0 - 2020-07-21
|
||||
>>>>
|
||||
>>>> * Add support for using named ids [Pagan Gazzard]
|
||||
>>>>
|
||||
>>> </details>
|
||||
>>>
|
||||
>>>
|
||||
>>> ##### balena-request-11.1.0 - 2020-07-16
|
||||
>>>
|
||||
>>> * Add lazy loading for most modules [Pagan Gazzard]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>>
|
||||
>> #### balena-sdk-15.0.3 - 2020-07-27
|
||||
>>
|
||||
>> * typings: Fix the PineWithSelect & related type helpers [Thodoris Greasidis]
|
||||
>> * typings: Use the native TypeScript Omit type helper [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-15.0.2 - 2020-07-22
|
||||
>>
|
||||
>> * Fix code snippet for initializing balena-sdk [Vipul Gupta (@vipulgupta2048)]
|
||||
>>
|
||||
>> #### balena-sdk-15.0.1 - 2020-07-15
|
||||
>>
|
||||
>> * Fix SupportTier/includes__SLA typing [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-15.0.0 - 2020-07-15
|
||||
>>
|
||||
>> * **BREAKING** Export setSharedOptions & fromSharedOptions separately [Thodoris Greasidis]
|
||||
>> * **BREAKING** Export as an ES6 module [Thodoris Greasidis]
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update dependencies and switch all returned promises to native promises [Pagan Gazzard] </summary>
|
||||
>>
|
||||
>>> ##### balena-auth-4.0.2 - 2020-07-13
|
||||
>>>
|
||||
>>> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-auth-4.0.1 - 2020-07-03
|
||||
>>>
|
||||
>>> * Explicitly add tslib dependency [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-auth-4.0.0 - 2020-07-02
|
||||
>>>
|
||||
>>> * Update to balena-settings-storage 6.x [Pagan Gazzard]
|
||||
>>> * Update target to es2015 [Pagan Gazzard]
|
||||
>>> * Switch to native promises [Pagan Gazzard]
|
||||
>>> * Enable strict type checking [Pagan Gazzard]
|
||||
>>> * Specify node 10+ [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-auth-3.1.1 - 2020-07-02
|
||||
>>>
|
||||
>>> * Switch to @balena/lint for linting [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-pine-12.1.1 - 2020-07-13
|
||||
>>>
|
||||
>>> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-pine-12.1.0 - 2020-07-06
|
||||
>>>
|
||||
>>> * Update balena-auth to 4.x and balena-request to 11.x [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-pine-12.0.1 - 2020-07-03
|
||||
>>>
|
||||
>>> * Use typescript import helpers [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-pine-12.0.0 - 2020-06-26
|
||||
>>>
|
||||
>>> * Stop actively supporting node 8 [Thodoris Greasidis]
|
||||
>>> * Convert to async await [Thodoris Greasidis]
|
||||
>>> * Add balenaCI repo.yml [Thodoris Greasidis]
|
||||
>>> * karma.conf.js: Combine declaration & assignment of karmaConfig [Thodoris Greasidis]
|
||||
>>> * Bump @balena/lint to v5 [Thodoris Greasidis]
|
||||
>>> * Drop getPine() in favor of an es6 export of the BalenaPine class [Thodoris Greasidis]
|
||||
>>> * Drop the API_PREFIX property in favor of the apiPrefix [Thodoris Greasidis]
|
||||
>>> * Bump to pinejs-client v6 which requires es2015 & drops Bluebird promises [Thodoris Greasidis]
|
||||
>>>
|
||||
>>> ##### balena-pine-11.2.1 - 2020-06-15
|
||||
>>>
|
||||
>>> * Convert karma.conf to js [Thodoris Greasidis]
|
||||
>>> * Bump balena-config-karma to v3 [Thodoris Greasidis]
|
||||
>>>
|
||||
>>> ##### balena-register-device-7.1.0 - 2020-07-13
|
||||
>>>
|
||||
>>> * Switch from randomstring to uuid for generating device uuids [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-register-device-7.0.1 - 2020-07-13
|
||||
>>>
|
||||
>>> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-register-device-7.0.0 - 2020-07-06
|
||||
>>>
|
||||
>>> * Convert to type checked javascript [Pagan Gazzard]
|
||||
>>> * Drop callback interface in favor of promise interface [Pagan Gazzard]
|
||||
>>> * Switch to a named export [Pagan Gazzard]
|
||||
>>> * Convert to typescript [Pagan Gazzard]
|
||||
>>> * Update to typed-error 3.x [Pagan Gazzard]
|
||||
>>> * Switch to returning native promises [Pagan Gazzard]
|
||||
>>> * Update to balena-request 11.x [Pagan Gazzard]
|
||||
>>> * Use typescript import helpers [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-register-device-6.1.6 - 2020-05-26
|
||||
>>>
|
||||
>>> * Export ApiError [Cameron Diver]
|
||||
>>>
|
||||
>>> ##### balena-register-device-6.1.5 - 2020-05-21
|
||||
>>>
|
||||
>>> * Convert tests to js [Thodoris Greasidis]
|
||||
>>>
|
||||
>>> ##### balena-register-device-6.1.4 - 2020-05-21
|
||||
>>>
|
||||
>>> * Install typed-error v2 [Cameron Diver]
|
||||
>>>
|
||||
>>> ##### balena-register-device-6.1.3 - 2020-05-20
|
||||
>>>
|
||||
>>> * Extend API exception to include full response object [Miguel Casqueira]
|
||||
>>>
|
||||
>>> ##### balena-register-device-6.1.2 - 2020-05-20
|
||||
>>>
|
||||
>>> * Update mocha to fix node v12 deprecation warning [Thodoris Greasidis]
|
||||
>>>
|
||||
>>> ##### balena-request-11.0.4 - 2020-07-14
|
||||
>>>
|
||||
>>> * Fix body overwriting on nodejs [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-request-11.0.3 - 2020-07-13
|
||||
>>>
|
||||
>>> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-request-11.0.2 - 2020-07-06
|
||||
>>>
|
||||
>>> * Fix tslib dependency [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-request-11.0.1 - 2020-07-03
|
||||
>>>
|
||||
>>> * Fix passing baseUrl to refreshToken if the request uses an absolute url [Pagan Gazzard]
|
||||
>>>
|
||||
>>> ##### balena-request-11.0.0 - 2020-07-03
|
||||
>>>
|
||||
>>> * Convert to type checked javascript [Pagan Gazzard]
|
||||
>>> * Switch to returning native promises [Pagan Gazzard]
|
||||
>>> * Drop support for nodejs < 10 [Pagan Gazzard]
|
||||
>>> * Update balena-auth to 4.x [Pagan Gazzard]
|
||||
>>> * Remove rindle dependency [Pagan Gazzard]
|
||||
>>> * Update fetch-ponyfill to 6.x [Pagan Gazzard]
|
||||
>>> * Remove proxy tests as global-tunnel-ng only supports nodejs < 10.16.0 [Pagan Gazzard]
|
||||
>>> * Switch to a named export [Pagan Gazzard]
|
||||
>>> * Use typescript import helpers [Pagan Gazzard]
|
||||
>>> * Bump balena-config-karma & convert karma.conf.coffee to js [Thodoris Greasidis]
|
||||
>>> * Change the browser request timeout error to be consistent with node [Thodoris Greasidis]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>> * **BREAKING** billing: Make the organization parameter fist & required [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-14.8.0 - 2020-07-15
|
||||
>>
|
||||
>> * DeviceWithServiceDetails: preserve the image_install & gateway_downloads [Thodoris Greasidis]
|
||||
>> * typings: Deprecate DeviceWithImageInstalls in favor of the Device type [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-14.7.1 - 2020-07-14
|
||||
>>
|
||||
>> * Fix is_private typings for device type [Stevche Radevski]
|
||||
>>
|
||||
>> #### balena-sdk-14.7.0 - 2020-07-14
|
||||
>>
|
||||
>> * Add an organization parameter to all billing methods [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-14.6.0 - 2020-07-13
|
||||
>>
|
||||
>> * typings: Add ApplicationHostedOnApplication [Thodoris Greasidis]
|
||||
>> * typings Add RecoveryTwoFactor [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-14.5.1 - 2020-07-10
|
||||
>>
|
||||
>> * Tests: remove bluebird usage [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.5.0 - 2020-07-09
|
||||
>>
|
||||
>> * tests/integration/setup: Convert to TypeScript [Thodoris Greasidis]
|
||||
>> * typings/ImageInstall: Deprecate the image field [Thodoris Greasidis]
|
||||
>> * typings/ImageInstall: Add the `installs__image` field [Thodoris Greasidis]
|
||||
>> * typings: Add typings for the ReleaseImage [Thodoris Greasidis]
|
||||
>> * typings/ImageInstall: Add the missing device property [Thodoris Greasidis]
|
||||
>> * Convert all remaining tests away from coffeescript [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.4.2 - 2020-07-09
|
||||
>>
|
||||
>> * Tests: improve typing for access to private SDK os methods [Pagan Gazzard]
|
||||
>> * Tests: improve typing of tag helpers [Pagan Gazzard]
|
||||
>> * Tests: import BalenaSDK types directly [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.4.1 - 2020-07-08
|
||||
>>
|
||||
>> * Tests: merge multiple application deletions into a single call [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.4.0 - 2020-07-08
|
||||
>>
|
||||
>> * Improve typings for `sdk.pine.post` [Pagan Gazzard]
|
||||
>> * Improve typings for `sdk.request` [Pagan Gazzard]
|
||||
>> * Improve typings for `models.device.getOsVersion` [Pagan Gazzard]
|
||||
>> * Improve typings for `models.device.lastOnline` [Pagan Gazzard]
|
||||
>> * Fix typings for `models.device.getMACAddresses` [Pagan Gazzard]
|
||||
>> * Fix typings for `models.device.getLocalIPAddresses` [Pagan Gazzard]
|
||||
>> * Add typings for `models.application.getDashboardUrl` [Pagan Gazzard]
|
||||
>> * Device model: last_connectivity_event and os_version can be null [Pagan Gazzard]
|
||||
>> * Improve typings for `models.device.getLocalModeSupport` [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.3.3 - 2020-07-07
|
||||
>>
|
||||
>> * Minimize bluebird sugar usage [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.3.2 - 2020-07-07
|
||||
>>
|
||||
>> * Add type checking for tests [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.3.1 - 2020-07-07
|
||||
>>
|
||||
>> * Tests: cache device type lookup [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.3.0 - 2020-07-07
|
||||
>>
|
||||
>> * typings: Export pine variant w/ a mandatory $select on get requests [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.9 - 2020-07-07
|
||||
>>
|
||||
>> * Remove `this.skip` usage as a faster workaround to afterEach skipping [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.8 - 2020-07-06
|
||||
>>
|
||||
>> * Improve internal typings by avoiding some `any` cases [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.7 - 2020-07-06
|
||||
>>
|
||||
>> * Include typings for all lazy loaded requires [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.6 - 2020-07-06
|
||||
>>
|
||||
>> * Simplify balena-request custom typings [Pagan Gazzard]
|
||||
>> * Use import type for declaration imports [Pagan Gazzard]
|
||||
>> * Simplify balena-pine custom typings [Pagan Gazzard]
|
||||
>> * Import balena-sdk type declarations via import type and not direct path [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.5 - 2020-07-06
|
||||
>>
|
||||
>> * Use typescript import helpers [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.4 - 2020-07-03
|
||||
>>
|
||||
>> * Drop dtslint in favor of plain @ts-expect-error [Thodoris Greasidis]
|
||||
>> * Enable strict checks for the typing tests [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.3 - 2020-07-03
|
||||
>>
|
||||
>> * Standardize bluebird naming as `Bluebird` [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.2 - 2020-07-03
|
||||
>>
|
||||
>> * Avoid $ExpectType b/c of issues with TS 3.9.6 [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.1 - 2020-07-01
|
||||
>>
|
||||
>> * model: Add build_environment_variable [Rich Bayliss]
|
||||
>>
|
||||
>> #### balena-sdk-14.2.0 - 2020-07-01
|
||||
>>
|
||||
>> * Add typings for plans & subscriptions [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-image-fs-7.0.1 - 2020-08-04
|
||||
>>
|
||||
>> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-image-fs-7.0.0 - Invalid date
|
||||
>>
|
||||
>> * Stop using lodash [Alexis Svinartchouk]
|
||||
>> * Simplify, only leave the interact function [Alexis Svinartchouk]
|
||||
>> * Convert to typescript [Alexis Svinartchouk]
|
||||
>> * lint on pre-commit [Alexis Svinartchouk]
|
||||
>> * Rename resin -> balena [Alexis Svinartchouk]
|
||||
>> * Replace wary with mocha [Alexis Svinartchouk]
|
||||
>>
|
||||
>> #### balena-image-fs-6.0.0 - 2020-07-24
|
||||
>>
|
||||
>> * Update file-disk to ^7 [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-image-manager-7.0.3 - 2020-08-04
|
||||
>
|
||||
> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-image-manager-7.0.2 - 2020-08-04
|
||||
>
|
||||
> * Update balena-sdk to 15.x [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-preload-10.2.4 - 2020-08-10
|
||||
>
|
||||
> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-preload-10.2.3 - 2020-08-04
|
||||
>
|
||||
> * Fix build dir paths [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-preload-10.2.2 - 2020-08-04
|
||||
>
|
||||
> * Update balena-sdk to 15.x [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-preload-10.2.1 - 2020-08-04
|
||||
>
|
||||
> * Add type checking [Pagan Gazzard]
|
||||
> * Use @balena/lint for linting [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sync-11.0.2 - 2020-08-04
|
||||
>
|
||||
> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sync-11.0.1 - 2020-08-04
|
||||
>
|
||||
> * Update balena-sdk to 15.x [Pagan Gazzard]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.14.4 - 2020-08-11
|
||||
|
||||
* Tests: Specify es2018 as preferred, matching normal usage [Pagan Gazzard]
|
||||
* Tests: Use a tmp data dir to avoid conflicts/overwriting existing data [Pagan Gazzard]
|
||||
* Tests: Use a mocha file helper to always load config-tests first [Pagan Gazzard]
|
||||
* Tests: Share mocha options between commands in package.json [Pagan Gazzard]
|
||||
|
||||
## 12.14.3 - 2020-08-11
|
||||
|
||||
* CI: Avoid duplicate windows/mac tests [Pagan Gazzard]
|
||||
|
||||
## 12.14.2 - 2020-08-10
|
||||
|
||||
* Lazy load tar-stream [Pagan Gazzard]
|
||||
|
219
CONTRIBUTING.md
219
CONTRIBUTING.md
@ -2,10 +2,12 @@
|
||||
|
||||
The balena CLI is an open source project and your contribution is welcome!
|
||||
|
||||
* Install the dependencies listed in the [NPM Installation](./INSTALL.md#npm-installation)
|
||||
section of the `INSTALL.md` file. Check the section [Additional
|
||||
Dependencies](./INSTALL.md#additional-dependencies) too.
|
||||
* Clone the `balena-cli` repository, `cd` to it and run `npm install`.
|
||||
* Install the dependencies listed in the [NPM Installation
|
||||
section](./INSTALL-ADVANCED.md#npm-installation) section of the installation instructions. Check
|
||||
the section [Additional Dependencies](./INSTALL-ADVANCED.md#additional-dependencies) too.
|
||||
* Clone the `balena-cli` repository (or a [forked
|
||||
repo](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo),
|
||||
if you are not in the balena team), `cd` to it and run `npm install`.
|
||||
* Build the CLI with `npm run build` or `npm test`, and execute it with `./bin/balena`
|
||||
(on a Windows command prompt, you may need to run `node .\bin\balena`).
|
||||
|
||||
@ -19,44 +21,89 @@ Before opening a PR, test your changes with `npm test`. Keep compatibility in mi
|
||||
meant to run on Linux, macOS and Windows. balena CI will run test code on all three platforms, but
|
||||
this will only help if you add some test cases for your new code!
|
||||
|
||||
## ./bin/balena-dev and oclif
|
||||
## Semantic versioning, commit messages and the ChangeLog
|
||||
|
||||
When using `./bin/balena-dev` with oclif-converted commands, it is currently necessary to manually
|
||||
edit the `oclif` section of `package.json` to replace `./build` with `./lib` as follows:
|
||||
When a pull request is merged, Balena's versionbot / Continuous Integration system takes care of
|
||||
automatically creating a new CLI release on both the [npm
|
||||
registry](https://www.npmjs.com/package/balena-cli) and the GitHub [releases
|
||||
page](https://github.com/balena-io/balena-cli/releases). The release version numbering adheres to
|
||||
the [Semantic Versioning's](http://semver.org/) concept of patch, minor and major releases.
|
||||
Generally, bug fixes and documentation changes are classed as patch changes, while new features are
|
||||
classed as minor changes. If a change breaks backwards compatibility, it is a major change.
|
||||
|
||||
Change from:
|
||||
```
|
||||
"oclif": {
|
||||
"commands": "./build/actions-oclif",
|
||||
"hooks": {
|
||||
"prerun": "./build/hooks/prerun/track"
|
||||
```
|
||||
A new version entry is also automatically added to the
|
||||
[CHANGELOG.md](https://github.com/balena-io/balena-cli/blob/master/CHANGELOG.md) file when a pull
|
||||
request is merged. Each pull request corresponds to a single version / release. Each commit in the
|
||||
pull request becomes a bullet point entry in the Changelog. The Changelog file should not be
|
||||
manually edited.
|
||||
|
||||
To:
|
||||
```
|
||||
"oclif": {
|
||||
"commands": "./lib/actions-oclif",
|
||||
"hooks": {
|
||||
"prerun": "./lib/hooks/prerun/track"
|
||||
```
|
||||
To support this automation, a commit message should be structured as follows:
|
||||
|
||||
And then remember to change it back before pushing the pull request. This is obviously error prone
|
||||
and inconvenient, and improvement suggestions are welcome: is there a better solution than
|
||||
automatically editing `package.json`? It is doable, if it is what needs to be done.
|
||||
```text
|
||||
The first line becomes a bullet point in the CHANGELOG file
|
||||
|
||||
## Semantic versioning and commit messages
|
||||
Optionally, a more detailed description in one or more paragraphs.
|
||||
The detailed description can be seen with `git log`, but it is not copied
|
||||
to the CHANGELOG file.
|
||||
|
||||
The CLI version numbering adheres to [Semantic Versioning](http://semver.org/). The following
|
||||
header/row is required in the body of a commit message, and will cause the CI build to fail if absent:
|
||||
|
||||
```
|
||||
Change-type: patch|minor|major
|
||||
```
|
||||
|
||||
Version numbers and commit messages are automatically added to the `CHANGELOG.md` file by the CI
|
||||
build flow, after a pull request is merged. It should not be manually edited.
|
||||
Only the first line of the commit message is copied to the Changelog file. The `Change-type` footer
|
||||
must be preceded by a blank line, and indicates the commit's semver change type. When a PR consists
|
||||
of multiple commits, the commits may have different change type values. As a whole, the PR will
|
||||
produce a release of the "highest" change type. For example, two commits mixing patch and minor
|
||||
change types will produce a minor CLI release, while two commits mixing minor and major change
|
||||
types will produce a major CLI release.
|
||||
|
||||
## Editing documentation files (CHANGELOG, README, website...)
|
||||
The commit message is parsed / checked by versionbot with the
|
||||
[resin-commit-lint](https://github.com/balena-io-modules/resin-commit-lint#resin-commit-lint)
|
||||
package.
|
||||
|
||||
Because of the way that the Changelog file is automatically updated from commit messages, which
|
||||
become the source of "what's new" for CLI end users, we advocate "meaningful commits" and
|
||||
user-focused commit messages. A meaningful commit is one that, in isolation, introduces a fix or
|
||||
feature (or part of a fix or feature) that makes sense at the Changelog level, and which leaves the
|
||||
CLI in a non-broken state. Sometimes, in the course of preparing a single pull request, a developer
|
||||
creates several commits as a way of saving their "work in progress", which may even fail to build
|
||||
(e.g. `npm run build` fails), and which is then fixed or undone by further commits in the same PR.
|
||||
In this situation, the recommendation is to "squash" or "fixup" the work-in-progress commits into
|
||||
fewer, meaningful commits. Interactive rebase is a good tool to achieve this:
|
||||
[blog](https://thoughtbot.com/blog/git-interactive-rebase-squash-amend-rewriting-history),
|
||||
[docs](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History).
|
||||
|
||||
Mixing multiple distinct features or bug fixes in a single commit is discouraged, because the
|
||||
description will likely not fit in the single-line Changelog bullet point and also because it
|
||||
makes it harder to review the pull request (especially a large one) and harder to isolate and
|
||||
revert individual changes in case a bug is found later on. Create a separate commit for each
|
||||
feature / bug fix, or even separate pull requests.
|
||||
|
||||
If you need to catch up with changes to the master branch while working on a pull request,
|
||||
use rebase instead of merge: [docs](https://git-scm.com/book/en/v2/Git-Branching-Rebasing).
|
||||
|
||||
If `package.json` is updated for dependencies listed in the `repo.yml` file (like `balena-sdk`),
|
||||
the commit message body should also include a line in the following format:
|
||||
```
|
||||
Update balena-sdk from 12.0.0 to 12.1.0
|
||||
```
|
||||
|
||||
This allows versionbot to produce nested Changelog entries (with expandable arrows), pulling in
|
||||
commit messages from the upstream repositories. The following npm script can be used to
|
||||
automatically produce a commit with a suitable commit message:
|
||||
```
|
||||
npm run update balena-sdk ^12.1.0
|
||||
```
|
||||
|
||||
The script will create a new branch (only if `master` is currently checked out), run `npm update`
|
||||
with the given target version and commit the `package.json` and `npm-shrinkwrap.json` files. The
|
||||
script by default will set the `Change-type` to `patch` or `minor`, depending on the semver change
|
||||
of the updated dependency. A `major` change type can specified as an extra argument:
|
||||
```
|
||||
npm run update balena-sdk ^12.14.0 patch
|
||||
npm run update balena-sdk ^13.0.0 major
|
||||
```
|
||||
|
||||
## Editing documentation files (README, INSTALL, Reference website...)
|
||||
|
||||
The `doc/cli.markdown` file is automatically generated by running `npm run build:doc` (which also
|
||||
runs as part of `npm run build`). That file is then pulled by scripts in the
|
||||
@ -65,61 +112,56 @@ Documentation page](https://www.balena.io/docs/reference/cli/).
|
||||
|
||||
The content sources for the auto generation of `doc/cli.markdown` are:
|
||||
|
||||
* Selected sections of the README file.
|
||||
* The CLI's command documentation in source code (both Capitano and oclif commands), for example:
|
||||
* `lib/actions/build.coffee`
|
||||
* `lib/actions-oclif/env/add.ts`
|
||||
* [Selected
|
||||
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
||||
of the README file.
|
||||
* The CLI's command documentation in source code (`lib/commands/` folder), for example:
|
||||
* `lib/commands/push.ts`
|
||||
* `lib/commands/env/add.ts`
|
||||
|
||||
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
||||
`doc/cli.markdown` by the `getCapitanoDoc()` function in
|
||||
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
||||
|
||||
The `INSTALL.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
|
||||
## Windows
|
||||
|
||||
Please note that `npm run build:installer` (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 you make changes to `package.json` scripts, check
|
||||
they also run on a standard Windows Command Prompt.
|
||||
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.
|
||||
|
||||
## Updating the 'npm-shrinkwrap.json' file
|
||||
|
||||
The `npm-shrinkwrap.json` file is used to control package dependencies, as documented at
|
||||
https://docs.npmjs.com/files/shrinkwrap.json.
|
||||
|
||||
While developing, the `package.json` file is often modified by, or before, running `npm install`
|
||||
in order to add, remove or modify dependencies. When `npm install` is executed, it automatically
|
||||
updates the `npm-shrinkwrap.json` file as well, **taking into account not only the `package.json`
|
||||
file but also the current state of the `node_modules` folder in your computer.**
|
||||
Changes to `npm-shrinkwrap.json` can be automatically merged by git during operations like
|
||||
`rebase`, `pull` and `cherry-pick`, but in some cases this results in suboptimal dependency
|
||||
resolution (the `node_modules` folder may end up larger than necessary, with consequences to CLI
|
||||
load time too). For this reason, the recommended way to update `npm-shrinkwrap.json` is to run
|
||||
`npm install`, possibly alongside `npm dedupe` as well. The following commands can be used to
|
||||
fix shrinkwrap issues and optimize the dependencies:
|
||||
|
||||
Meanwhile, as a text (JSON) file, `git` is capable of merging the `npm-shrinkwrap.json` file during
|
||||
operations like `rebase`, `cherry-pick` and `pull`. But git's automated merge is not the
|
||||
recommended way of updating the `npm-shrinkwrap.json` file, because it does not take into account
|
||||
duplicates or conflicts in the dependency tree, or indeed the state of the `package.json` file
|
||||
(which may have just been merged). In extreme cases, the automated merge may actually result in a
|
||||
broken installation. For these reasons, automatic merging of the `npm-shrinkwrap.json` was disabled
|
||||
through the `.gitattributes` file (the "binary merge driver" allows diff'ing but prevents automatic
|
||||
merging). Operations like `git rebase` may then result in an error like:
|
||||
|
||||
```text
|
||||
$ git rebase master
|
||||
warning: Cannot merge binary files: npm-shrinkwrap.json (HEAD vs. c34942b9... test)
|
||||
Auto-merging npm-shrinkwrap.json
|
||||
CONFLICT (content): Merge conflict in npm-shrinkwrap.json
|
||||
error: Failed to merge in the changes.
|
||||
```sh
|
||||
git checkout master -- npm-shrinkwrap.json
|
||||
rm -rf node_modules
|
||||
npm install # update npm-shrinkwrap.json to satisfy changes to package.json
|
||||
npm dedupe # deduplicate dependencies from npm-shrinkwrap.json
|
||||
npm install # re-add optional dependencies removed by dedupe
|
||||
git add npm-shrinkwrap.json # add it for committing (solve merge errors)
|
||||
```
|
||||
|
||||
Whether or not there is a merge error, the following commands are the recommended way of updating
|
||||
and committing the `npm-shrinkwrap.json` file:
|
||||
Note that `npm dedupe` should always be followed by `npm install`, as shown above, even if
|
||||
`npm install` had already been executed before `npm dedupe`.
|
||||
|
||||
```bash
|
||||
$ rm -rf node_modules # Linux / Mac
|
||||
$ rmdir /s node_modules # Windows Command Prompt
|
||||
$ npm checkout master -- npm-shrinkwrap.json # revert it to the master branch state
|
||||
$ npm install # "cleanly" update the npm-shrinkwrap.json file
|
||||
$ git add npm-shrinkwrap.json # add it for committing (solve merge errors)
|
||||
Optionally, these steps may be automated by installing the
|
||||
[npm-merge-driver](https://www.npmjs.com/package/npm-merge-driver):
|
||||
|
||||
```sh
|
||||
npx npm-merge-driver install -g
|
||||
```
|
||||
|
||||
## TypeScript and oclif
|
||||
@ -128,18 +170,12 @@ The CLI currently contains a mix of plain JavaScript and
|
||||
[TypeScript](https://www.typescriptlang.org/) code. The goal is to have all code written in
|
||||
Typescript, in order to take advantage of static typing and formal programming interfaces.
|
||||
The migration towards Typescript is taking place gradually, as part of maintenance work or
|
||||
the implementation of new features. Historically, the CLI was originally written in
|
||||
[CoffeeScript](https://coffeescript.org), but all CoffeeScript code was migrated to either
|
||||
Javascript or Typescript.
|
||||
the implementation of new features.
|
||||
|
||||
Similarly, [Capitano](https://github.com/balena-io/capitano) was originally adopted as the CLI's
|
||||
framework, but later we decided to take advantage of [oclif](https://oclif.io/)'s features such
|
||||
as native installers for Windows, macOS and Linux, and support for custom flag parsing (for
|
||||
example, we're still battling with Capitano's behavior of dropping leading zeros of arguments that
|
||||
look like integers, such as some abbreviated UUIDs). Again, the migration is taking place
|
||||
gradually, with some CLI commands parsed by oclif and others by Capitano. A simple command line
|
||||
pre-parsing takes place in `preparser.ts`, to decide whether to route full parsing to Capitano or
|
||||
to oclif.
|
||||
Of historical interest, the CLI was originally written in [CoffeeScript](https://coffeescript.org)
|
||||
and used the [Capitano](https://github.com/balena-io/capitano) framework. All CoffeeScript code was
|
||||
migrated to either Javascript or Typescript, and Capitano was replaced with oclif. A few file or
|
||||
variable names still refer to this legacy, for example `automation/capitanodoc/capitanodoc.ts`.
|
||||
|
||||
## Programming style
|
||||
|
||||
@ -147,29 +183,6 @@ to oclif.
|
||||
reformats the code. Beyond that, we have a preference for Javascript promises over callbacks, and for
|
||||
`async/await` over `.then()`.
|
||||
|
||||
## Updating upstream dependencies
|
||||
|
||||
In order to get proper nested changelogs, when updating upstream modules that are in the repo.yml
|
||||
(like the balena-sdk), the commit body has to contain a line with the following format:
|
||||
```
|
||||
Update balena-sdk from 12.0.0 to 12.1.0
|
||||
```
|
||||
|
||||
Since this is error prone, it's suggested to use the following npm script:
|
||||
```
|
||||
npm run update balena-sdk ^12.1.0
|
||||
```
|
||||
|
||||
This will create a new branch (only if you are currently on master), run `npm update` with the
|
||||
version you provided as a target and commit the package.json & npm-shrinkwrap.json. The script by
|
||||
default will set the `Change-type` to `patch` or `minor`, depending on the semver change of the
|
||||
updated dependency, but if you need to use a different one (eg `major`) you can specify it as an
|
||||
extra argument:
|
||||
```
|
||||
npm run update balena-sdk ^12.14.0 patch
|
||||
npm run update balena-sdk ^13.0.0 major
|
||||
```
|
||||
|
||||
## Common gotchas
|
||||
|
||||
One thing that most CLI bugs have in common is the absence of test cases exercising the broken
|
||||
|
150
INSTALL-ADVANCED.md
Normal file
150
INSTALL-ADVANCED.md
Normal file
@ -0,0 +1,150 @@
|
||||
# balena CLI Advanced Installation Options
|
||||
|
||||
**These are alternative, advanced installation options. Most users would prefer the [recommended,
|
||||
streamlined installation
|
||||
instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).**
|
||||
|
||||
There are 3 options to choose from to install balena's CLI:
|
||||
|
||||
* [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the
|
||||
traditional graphical desktop application installers.
|
||||
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
|
||||
executable in them: extract and run. Available for all platforms: Linux, Windows, macOS.
|
||||
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.
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
|
||||
## Executable Installer
|
||||
|
||||
This is the recommended installation option on macOS and Windows. Follow the specific OS
|
||||
instructions:
|
||||
|
||||
* [Windows](./INSTALL-WINDOWS.md)
|
||||
* [macOS](./INSTALL-MAC.md)
|
||||
|
||||
> Note regarding WSL ([Windows Subsystem for
|
||||
> Linux](https://docs.microsoft.com/en-us/windows/wsl/about))
|
||||
> If you would like to use WSL, follow the [installations instructions for
|
||||
> Linux](./INSTALL-LINUX.md) rather than Windows, as WSL consists of a Linux environment.
|
||||
|
||||
If you had previously installed the CLI using a standalone zip package, it may be a good idea to
|
||||
check your system's `PATH` environment variable for duplicate entries, as the terminal will use the
|
||||
entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package) instructions
|
||||
for how to modify the PATH variable.
|
||||
|
||||
By default, the CLI is installed to the following folders:
|
||||
|
||||
OS | Folders
|
||||
--- | ---
|
||||
Windows: | `C:\Program Files\balena-cli\`
|
||||
macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
|
||||
## Standalone Zip Package
|
||||
|
||||
1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with the word "standalone", for example:
|
||||
`balena-cli-vX.Y.Z-linux-x64-standalone.zip` ← _also for the Windows Subsystem for Linux_
|
||||
`balena-cli-vX.Y.Z-macOS-x64-standalone.zip`
|
||||
`balena-cli-vX.Y.Z-windows-x64-standalone.zip`
|
||||
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable.
|
||||
See instructions for:
|
||||
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
|
||||
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
|
||||
[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).
|
||||
> * **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.
|
||||
> * Note that moving the `balena` executable out of the extracted `balena-cli` folder on its own
|
||||
> (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other
|
||||
> folders and files also present in the `balena-cli` folder.
|
||||
|
||||
To update the CLI to a new version, download a new release zip file and replace the previous
|
||||
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
||||
as described above.
|
||||
|
||||
## NPM Installation
|
||||
|
||||
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:
|
||||
|
||||
* [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". This [sample
|
||||
Dockerfile](https://gist.github.com/pdcastro/5d4d96652181e7da685a32caf629dd44) shows the CLI
|
||||
installation steps on an Ubuntu 18.04 base image.
|
||||
* [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`
|
||||
|
||||
On **Windows (not WSL),** the dependencies above and additional ones can be met by installing:
|
||||
|
||||
* 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`
|
||||
* [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 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`
|
||||
|
||||
With these dependencies in place, the balena CLI installation command is:
|
||||
|
||||
```sh
|
||||
$ npm install balena-cli -g --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).
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
The `balena ssh`, `scan`, `build`, `deploy`, `preload` and `os configure` commands may require
|
||||
additional software to be installed. Check the Additional Dependencies sections for each operating
|
||||
system:
|
||||
|
||||
* [Windows](./INSTALL-WINDOWS.md#additional-dependencies)
|
||||
* [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
|
||||
may be desirable include:
|
||||
|
||||
* To avoid having to install Docker on the development machine / laptop.
|
||||
* To take advantage of a more powerful server (CPU, memory).
|
||||
* 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
|
||||
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).
|
66
INSTALL-LINUX.md
Normal file
66
INSTALL-LINUX.md
Normal file
@ -0,0 +1,66 @@
|
||||
# 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).
|
||||
|
||||
Selected operating system: **Linux**
|
||||
|
||||
1. Download the latest zip file from the [latest release
|
||||
page](https://github.com/balena-io/balena-cli/releases/latest). Look for a file name that ends
|
||||
with "-standalone.zip", for example:
|
||||
`balena-cli-vX.Y.Z-linux-x64-standalone.zip`
|
||||
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable. There are several
|
||||
ways of achieving this on Linux: See this [StackOverflow post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix). Close and reopen the terminal window
|
||||
so that the changes to PATH can take effect.
|
||||
|
||||
4. 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
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
|
||||
`deploy` and `preload` commands may require additional software to be installed, as described
|
||||
below.
|
||||
|
||||
To update the balena CLI to a new version, download a new release zip file and replace the previous
|
||||
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
||||
as described above.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
### 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 simply follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
|
||||
machine) where the balena CLI is installed. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
The `balena ssh` command requires the `ssh` command-line tool to be available. Most Linux
|
||||
distributions will already have it installed. Otherwise, `sudo apt-get install openssh-client`
|
||||
should do the trick on Debian or Ubuntu.
|
||||
|
||||
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena scan
|
||||
|
||||
The `balena scan` command requires a multicast DNS (mDNS) service like
|
||||
[Avahi](https://en.wikipedia.org/wiki/Avahi_(software)), which is installed by default on most
|
||||
desktop Linux distributions. Otherwise, on Debian or Ubuntu, the installation command would be
|
||||
`sudo apt-get install avahi-daemon`.
|
||||
|
||||
### 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).
|
68
INSTALL-MAC.md
Normal file
68
INSTALL-MAC.md
Normal file
@ -0,0 +1,68 @@
|
||||
# balena CLI Installation Instructions for macOS
|
||||
|
||||
These instructions are for the recommended installation option. Advanced users may also be
|
||||
interested in [advanced installation options](./INSTALL-ADVANCED.md).
|
||||
|
||||
Selected operating system: **macOS**
|
||||
|
||||
1. Download the installer from the [latest release
|
||||
page](https://github.com/balena-io/balena-cli/releases/latest).
|
||||
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).
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## 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 simply follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
|
||||
machine) where the balena CLI is installed. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
The `balena ssh` command requires the `ssh` command-line tool to be available. To check whether
|
||||
it is already installed, run `ssh` on a Terminal window. If it is not yet installed, the options
|
||||
include:
|
||||
|
||||
* Download the Xcode Command Line Tools from https://developer.apple.com/downloads
|
||||
* Or, if you have Xcode installed, open Xcode, choose Preferences → General → Downloads →
|
||||
Components → Command Line Tools → Install.
|
||||
* Or, install [Homebrew](https://brew.sh/), then `brew install openssh`
|
||||
|
||||
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||
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
|
||||
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:
|
||||
|
||||
* 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.
|
82
INSTALL-WINDOWS.md
Normal file
82
INSTALL-WINDOWS.md
Normal file
@ -0,0 +1,82 @@
|
||||
# balena CLI Installation Instructions for Windows
|
||||
|
||||
These instructions are for the recommended installation option. Advanced users may also be
|
||||
interested in [advanced installation options](./INSTALL-ADVANCED.md).
|
||||
|
||||
Selected operating system: **Windows**
|
||||
|
||||
1. Download the installer from the [latest release
|
||||
page](https://github.com/balena-io/balena-cli/releases/latest).
|
||||
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).
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
described below.
|
||||
|
||||
## 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 simply follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
|
||||
machine) where the balena CLI is installed. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
The `balena ssh` command requires the `ssh` command-line tool to be available. Microsoft started
|
||||
distributing an SSH client with Windows 10, which is automatically installed through Windows
|
||||
Update. To check whether it is installed, run `ssh` on a Windows Command Prompt or PowerShell. It
|
||||
can also be [manually
|
||||
installed](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse)
|
||||
if needed. For older versions of Windows, there are several ssh/OpenSSH clients provided by 3rd
|
||||
parties.
|
||||
|
||||
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena scan
|
||||
|
||||
The `balena scan` command requires a multicast DNS (mDNS) service like Apple's Bonjour.
|
||||
Many Windows machines will already have this service installed, as it is bundled in popular
|
||||
applications such as Skype (Wikipedia lists [several others](https://en.wikipedia.org/wiki/Bonjour_(software))).
|
||||
Otherwise, Bonjour for Windows can be downloaded and installed from: https://support.apple.com/kb/DL999
|
||||
|
||||
### 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
|
||||
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:
|
||||
|
||||
* 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).
|
235
INSTALL.md
235
INSTALL.md
@ -1,231 +1,12 @@
|
||||
# balena CLI Installation Instructions
|
||||
|
||||
There are 3 options to choose from to install balena's CLI:
|
||||
Please select your operating system:
|
||||
|
||||
* [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the
|
||||
traditional graphical desktop application installers.
|
||||
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
|
||||
executable in them: extract and run. Available for all platforms: Linux, Windows, macOS.
|
||||
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.
|
||||
* [Windows](./INSTALL-WINDOWS.md)
|
||||
* [macOS](./INSTALL-MAC.md)
|
||||
* [Linux](./INSTALL-LINUX.md)
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
|
||||
> **Windows users:**
|
||||
> * There is a [YouTube video tutorial](https://www.youtube.com/watch?v=2LApclXFqsg) for installing
|
||||
> and getting started with the balena CLI on Windows. (The video uses the standalone zip package
|
||||
> option.)
|
||||
> * If you are using Microsoft's [Windows Subsystem for
|
||||
> Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL), install a balena CLI release
|
||||
> for Linux rather than for Windows, like the standalone zip package for Linux. An installation
|
||||
> with the graphical executable installer for Windows will **not** work with WSL.
|
||||
|
||||
## Executable Installer
|
||||
|
||||
Recommended for Windows (but not Windows Subsystem for Linux) and macOS:
|
||||
|
||||
1. Download the latest installer from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with "-installer", for example:
|
||||
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
||||
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
||||
|
||||
2. Double click the downloaded file to run the installer.
|
||||
_If you are using macOS Catalina (10.15), [check this known issue and
|
||||
workaround](https://github.com/balena-io/balena-cli/issues/1479)._
|
||||
|
||||
3. 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. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
|
||||
* `balena version` - should print the installed CLI version
|
||||
* `balena help` - should print the balena CLI help
|
||||
|
||||
> Note: If you had previously installed the CLI using a standalone zip package, it may be a good
|
||||
> idea to check your system's `PATH` environment variable for duplicate entries, as the terminal
|
||||
> will use the entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package)
|
||||
> instructions for how to modify the PATH variable.
|
||||
|
||||
By default, the CLI is installed to the following folders:
|
||||
|
||||
OS | Folders
|
||||
--- | ---
|
||||
Windows: | `C:\Program Files\balena-cli\`
|
||||
macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
|
||||
## Standalone Zip Package
|
||||
|
||||
1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with the word "standalone", for example:
|
||||
`balena-cli-vX.Y.Z-linux-x64-standalone.zip` ← _also for the Windows Subsystem for Linux_
|
||||
`balena-cli-vX.Y.Z-macOS-x64-standalone.zip`
|
||||
`balena-cli-vX.Y.Z-windows-x64-standalone.zip`
|
||||
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable.
|
||||
See instructions for:
|
||||
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
|
||||
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm)
|
||||
|
||||
> * If you are using macOS Catalina (10.15), [check this known issue and
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/1479).
|
||||
> * **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.
|
||||
> It should however work with all "desktop" or "server" distributions, e.g. Ubuntu, Debian, Suse,
|
||||
> Fedora, Arch Linux and many more.
|
||||
> * Note that moving the `balena` executable out of the extracted `balena-cli` folder on its own
|
||||
> (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other
|
||||
> folders and files also present in the `balena-cli` folder.
|
||||
|
||||
To update the CLI to a new version, download a new release zip file and replace the previous
|
||||
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
||||
as described above.
|
||||
|
||||
## NPM Installation
|
||||
|
||||
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:
|
||||
|
||||
* [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". This [sample
|
||||
Dockerfile](https://gist.github.com/pdcastro/5d4d96652181e7da685a32caf629dd44) shows the CLI
|
||||
installation steps on an Ubuntu 18.04 base image.
|
||||
* [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`
|
||||
|
||||
On **Windows (not WSL),** the dependencies above and additional ones can be met by installing:
|
||||
|
||||
* 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`
|
||||
* [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 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`
|
||||
|
||||
With these dependencies in place, the balena CLI installation command is:
|
||||
|
||||
```sh
|
||||
$ npm install balena-cli -g --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).
|
||||
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
* The `balena ssh` command requires a recent version of the `ssh` command-line tool to be available:
|
||||
* macOS and Linux usually already have it installed. Otherwise, search for the available packages
|
||||
on your specific Linux distribution, or for the Mac consider the [Xcode command-line
|
||||
tools](https://developer.apple.com/xcode/features/) or [homebrew](https://brew.sh/).
|
||||
|
||||
* Microsoft started distributing an SSH client with Windows 10, which we understand is
|
||||
automatically installed through Windows Update, but can be manually installed too
|
||||
([more information](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse)).
|
||||
For other versions of Windows, there are several ssh/OpenSSH clients provided by 3rd parties.
|
||||
|
||||
* The [`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) is needed
|
||||
for the `balena ssh` command to work behind a proxy. It is available for Linux distributions
|
||||
like Ubuntu/Debian (`apt install proxytunnel`), and for macOS through
|
||||
[Homebrew](https://brew.sh/). Windows support is limited to the Windows Subsystem for Linux
|
||||
(e.g., by installing Ubuntu through the Microsoft App Store). Check the
|
||||
[README](https://github.com/balena-io/balena-cli/blob/master/README.md) file for proxy
|
||||
configuration instructions.
|
||||
|
||||
* The `balena preload`, `balena build` and `balena deploy --build` commands require
|
||||
[Docker](https://docs.docker.com/install/overview/) or [balenaEngine](https://www.balena.io/engine/)
|
||||
to be available:
|
||||
* The `balena preload` command requires the Docker Engine to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Docker Desktop for Mac and
|
||||
Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1, so
|
||||
the workaround is to downgrade to version 18.06.1 (links: [Docker CE for
|
||||
Windows](https://docs.docker.com/docker-for-windows/release-notes/#docker-community-edition-18061-ce-win73-2018-08-29)
|
||||
and [Docker CE for
|
||||
Mac](https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18061-ce-mac73-2018-08-29)).
|
||||
See more details in [CLI issue 1099](https://github.com/balena-io/balena-cli/issues/1099).
|
||||
* Commonly, Docker is installed on the same machine where the CLI is being used, but the
|
||||
`balena build` and `balena deploy` commands can also use a remote Docker Engine (daemon)
|
||||
or balenaEngine (which could be a remote device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images)) by specifying
|
||||
its IP address and port number as command-line options. Check the documentation for each
|
||||
command, e.g. `balena help build`, or the [online
|
||||
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).
|
||||
* If you are using Microsoft's [Windows Subsystem for
|
||||
Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL) and Docker Desktop for
|
||||
Windows, check the [FAQ item "Docker seems to be
|
||||
unavailable"](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md#docker-seems-to-be-unavailable-error-when-using-windows-subsystem-for-linux-wsl).
|
||||
|
||||
* The `balena scan` command requires a multicast DNS (mDNS) service like Bonjour or Avahi:
|
||||
* On Windows, check if 'Bonjour' is installed (Control Panel > Programs and Features).
|
||||
If not, you can download Bonjour for Windows from https://support.apple.com/kb/DL999
|
||||
* Most 'desktop' Linux distributions ship with [Avahi](https://en.wikipedia.org/wiki/Avahi_(software)).
|
||||
Search for the installation command for your distribution. E.g. for Ubuntu:
|
||||
`sudo apt-get install avahi-daemon`
|
||||
* macOS comes with [Bonjour](https://en.wikipedia.org/wiki/Bonjour_(software)) built-in.
|
||||
|
||||
* The `balena os configure` command is currently not supported on Windows natively. Windows users are advised
|
||||
to install the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL)
|
||||
with Ubuntu, and use the Linux release of the balena CLI.
|
||||
|
||||
|
||||
## Configuring SSH keys
|
||||
|
||||
The `balena ssh` command requires an SSH key to be added to your balena account. If you had
|
||||
already added a SSH key in order to [deploy with 'git push'](https://www.balena.io/docs/learn/getting-started/raspberrypi3/nodejs/#adding-an-ssh-key),
|
||||
then you are probably done and may skip this section. You can check whether you already have
|
||||
an SSH key in your balena account with the `balena keys` command, or by visiting the
|
||||
[balena web dashboard](https://dashboard.balena-cloud.com/), clicking on your name -> Preferences
|
||||
-> SSH Keys.
|
||||
|
||||
> Note: An "SSH key" actually consists of a public/private key pair. A typical name for the private
|
||||
> key file is "id_rsa", and a typical name for the public key file is "id_rsa.pub". Both key files
|
||||
> are saved to your computer (with the private key optionally protected by a password), but only
|
||||
> the public key is saved to your balena account. This means that if you change computers or
|
||||
> otherwise lose the private key, _you cannot recover the private key through your balena account._
|
||||
> You can however add new keys, and delete the old ones.
|
||||
|
||||
If you don't have an SSH key in your balena account:
|
||||
|
||||
* If you have an existing SSH key in your computer that you would like to use, you can add it
|
||||
to your balena account through the balena web dashboard (Preferences -> SSH Keys), or through
|
||||
the CLI itself:
|
||||
|
||||
```bash
|
||||
# Windows 10 (cmd.exe prompt) example:
|
||||
$ balena key add MyKey %userprofile%\.ssh\id_rsa.pub
|
||||
# Linux / macOS example:
|
||||
$ balena key add MyKey ~/.ssh/id_rsa.pub
|
||||
```
|
||||
|
||||
* To generate a new key, you can follow [GitHub's documentation](https://help.github.com/en/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent),
|
||||
skipping the step about adding the key to your GitHub account, and instead adding the key to
|
||||
your balena account as described above.
|
||||
> Note regarding WSL ([Windows Subsystem for
|
||||
> Linux](https://docs.microsoft.com/en-us/windows/wsl/about))
|
||||
> If you would like to use WSL, follow the installations instructions for Linux
|
||||
> rather than Windows, as WSL consists of a Linux environment.
|
||||
|
82
README.md
82
README.md
@ -1,31 +1,30 @@
|
||||
# balena CLI
|
||||
|
||||
The official balena CLI tool.
|
||||
The official balena Command Line Interface.
|
||||
|
||||
[](http://badge.fury.io/js/balena-cli)
|
||||
[](https://david-dm.org/balena-io/balena-cli)
|
||||
|
||||
## About
|
||||
|
||||
The balena CLI (Command-Line Interface) allows you to interact with the balenaCloud and the
|
||||
[balena API](https://www.balena.io/docs/reference/api/overview/) through a terminal window
|
||||
on Linux, macOS or Windows. You can also write shell scripts around it, or import its Node.js
|
||||
modules to use it programmatically.
|
||||
As an [open-source project on GitHub](https://github.com/balena-io/balena-cli/), your contribution
|
||||
is also welcome!
|
||||
The balena CLI is a Command Line Interface for [balenaCloud](https://www.balena.io/cloud/) or
|
||||
[openBalena](https://www.balena.io/open/). It is a software tool available for Windows, macOS and
|
||||
Linux, used through a command prompt / terminal window. It can be used interactively or invoked in
|
||||
scripts. The balena CLI builds on the [balena API](https://www.balena.io/docs/reference/api/overview/)
|
||||
and the [balena SDK](https://www.balena.io/docs/reference/sdk/node-sdk/), and can also be directly
|
||||
imported in Node.js applications. The balena CLI is an [open-source project on
|
||||
GitHub](https://github.com/balena-io/balena-cli/), and your contribution is also welcome!
|
||||
|
||||
## Installation
|
||||
|
||||
Check the [balena CLI installation instructions on GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
Check the [balena CLI installation instructions on
|
||||
GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Choosing a shell (command prompt/terminal)
|
||||
## Choosing a shell (command prompt/terminal)
|
||||
|
||||
On **Windows,** the standard Command Prompt (`cmd.exe`) and
|
||||
[PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell?view=powershell-6)
|
||||
are supported. We are aware of users also having a good experience with alternative shells,
|
||||
including:
|
||||
are supported. Alternative shells include:
|
||||
|
||||
* [MSYS2](https://www.msys2.org/):
|
||||
* Install additional packages with the command:
|
||||
@ -43,17 +42,17 @@ including:
|
||||
[comment](https://github.com/balena-io/balena-cli/issues/598#issuecomment-556513098).
|
||||
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
|
||||
(WSL). In this case, a Linux distribution like Ubuntu is installed via the Microsoft Store, and a
|
||||
balena CLI release **for Linux** is recommended. See
|
||||
[FAQ](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md) for using balena
|
||||
CLI with WSL and Docker Desktop for Windows.
|
||||
balena CLI release **for Linux** should be selected. See
|
||||
[FAQ](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md) for using the
|
||||
balena CLI with WSL and Docker Desktop for Windows.
|
||||
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. _Optionally,_ `bash` command
|
||||
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)
|
||||
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
|
||||
## 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:
|
||||
@ -62,7 +61,7 @@ new release to your application. Those commands require creating a CLI login ses
|
||||
$ balena login
|
||||
```
|
||||
|
||||
### Proxy support
|
||||
## Proxy support
|
||||
|
||||
HTTP(S) proxies can be configured through any of the following methods, in precedence order
|
||||
(from higher to lower):
|
||||
@ -88,19 +87,26 @@ HTTP(S) proxies can be configured through any of the following methods, in prece
|
||||
* The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as
|
||||
`BALENARC_PROXY`.
|
||||
|
||||
> Note: The `balena ssh` command has additional setup requirements to work behind a proxy.
|
||||
> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md),
|
||||
> and ensure that the proxy server is configured to allow proxy requests to ssh port 22, using
|
||||
> SSL encryption. For example, in the case of the [Squid](http://www.squid-cache.org/) proxy
|
||||
> server, it should be configured with the following rules in the `squid.conf` file:
|
||||
> `acl SSL_ports port 22`
|
||||
> `acl Safe_ports port 22`
|
||||
### Proxy setup for balena ssh
|
||||
|
||||
#### Proxy exclusion
|
||||
In order to work behind a proxy server, the `balena ssh` command requires the
|
||||
[`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) to be installed.
|
||||
`proxytunnel` is available for Linux distributions like Ubuntu/Debian (`apt install proxytunnel`),
|
||||
and for macOS through [Homebrew](https://brew.sh/). Windows support is limited to the [Windows
|
||||
Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (e.g., by installing
|
||||
Ubuntu through the Microsoft App Store).
|
||||
|
||||
Ensure that the proxy server is configured to allow proxy requests to ssh port 22, using
|
||||
SSL encryption. For example, in the case of the [Squid](http://www.squid-cache.org/) proxy
|
||||
server, it should be configured with the following rules in the `squid.conf` file:
|
||||
`acl SSL_ports port 22`
|
||||
`acl Safe_ports port 22`
|
||||
|
||||
### Proxy exclusion
|
||||
|
||||
The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations from proxying.
|
||||
|
||||
> * This feature requires balena CLI version 11.30.8 or later. In the case of the npm [installation
|
||||
> * This feature requires CLI version 11.30.8 or later. In the case of the npm [installation
|
||||
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
|
||||
> Node.js version 10.16.0 or later.
|
||||
> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the
|
||||
@ -129,25 +135,27 @@ address like `192.168.1.2`.
|
||||
## Command reference documentation
|
||||
|
||||
The full CLI command reference is available [on the web](https://www.balena.io/docs/reference/cli/
|
||||
) or by running `balena help` and `balena help --verbose`.
|
||||
) or by running `balena help --verbose`.
|
||||
|
||||
## Support, FAQ and troubleshooting
|
||||
|
||||
If you come across any problems or would like to get in touch:
|
||||
To learn more, troubleshoot issues, or to contact us for support:
|
||||
|
||||
* Check our [FAQ / troubleshooting document](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md).
|
||||
* Ask us a question through the [balenaCloud forum](https://forums.balena.io/c/balena-cloud).
|
||||
* For bug reports or feature requests,
|
||||
[have a look at the GitHub issues or create a new one](https://github.com/balena-io/balena-cli/issues/).
|
||||
* Check the [masterclass tutorials](https://www.balena.io/docs/learn/more/masterclasses/overview/)
|
||||
* Check our [FAQ / troubleshooting document](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md)
|
||||
* Ask us a question through the [balenaCloud forum](https://forums.balena.io/c/balena-cloud)
|
||||
|
||||
For CLI bug reports or feature requests, check the
|
||||
[CLI GitHub issues](https://github.com/balena-io/balena-cli/issues/).
|
||||
|
||||
## Deprecation policy
|
||||
|
||||
The balena CLI uses [semver versioning](https://semver.org/), with the concepts
|
||||
of major, minor and patch version releases.
|
||||
|
||||
The latest release of the previous major version of the balena CLI will remain
|
||||
compatible with the balenaCloud backend services for one year from the date when
|
||||
the next major version is released. For example, balena CLI v10.17.5, as the
|
||||
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.
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
# FAQ & Troubleshooting
|
||||
# balena CLI FAQ & Troubleshooting
|
||||
|
||||
This document contains some common issues, questions and answers related to the balena CLI.
|
||||
|
||||
## Where is my configuration file?
|
||||
## Where is the balena CLI's configuration file located?
|
||||
|
||||
The per-user configuration file lives in `$HOME/.balenarc.yml` or `%UserProfile%\_balenarc.yml`, in
|
||||
Unix based operating systems and Windows respectively.
|
||||
@ -10,53 +8,43 @@ Unix based operating systems and Windows respectively.
|
||||
The balena CLI also attempts to read a `balenarc.yml` file in the current directory, which takes
|
||||
precedence over the per-user configuration file.
|
||||
|
||||
## How do I point the balena CLI to staging?
|
||||
## How do I point the balena CLI to the staging environment?
|
||||
|
||||
The easiest way is to set the `BALENARC_BALENA_URL=balena-staging.com` environment variable.
|
||||
|
||||
Alternatively, you can edit your configuration file and set `balenaUrl: balena-staging.com` to
|
||||
persist this setting.
|
||||
Set the `BALENARC_BALENA_URL=balena-staging.com` environment variable, or add
|
||||
`balenaUrl: balena-staging.com` to the balena CLI's configuration file.
|
||||
|
||||
## How do I make the balena CLI persist data in another directory?
|
||||
|
||||
The balena CLI persists your session token, as well as cached images in `$HOME/.balena` or
|
||||
`%UserProfile%\_balena`.
|
||||
The balena CLI persists the session token, as well as cached assets, to `$HOME/.balena` or
|
||||
`%UserProfile%\_balena`. This directory can be changed by setting an environment variable,
|
||||
`BALENARC_DATA_DIRECTORY=/opt/balena`, or by adding `dataDirectory: /opt/balena` to the CLI's
|
||||
configuration file, replacing `/opt/balena` with the desired directory.
|
||||
|
||||
Pointing the balena CLI to persist data in another location is necessary in certain environments,
|
||||
like a server, where there is no home directory, or a device running balenaOS, which erases all
|
||||
data after a restart.
|
||||
## After burning to an SD card, my device doesn't boot
|
||||
|
||||
You can accomplish this by setting `BALENARC_DATA_DIRECTORY=/opt/balena` or adding `dataDirectory:
|
||||
/opt/balena` to your configuration file, replacing `/opt/balena` with your desired directory.
|
||||
Check whether the downloaded image is incomplete (download was interrupted) or corrupted.
|
||||
|
||||
## After burning to an sdcard, my device doesn't boot
|
||||
Try clearing the cache (`%HOME/.balena/cache` or `C:\Users\<user>\_balena\cache`) and running the
|
||||
command again.
|
||||
|
||||
- The downloaded image is not complete (download was interrupted).
|
||||
## I get a permission error when burning to an SD card
|
||||
|
||||
Please clean the cache (`%HOME/.balena/cache` or `C:\Users\<user>\_balena\cache`) and run the command again. In the future, the CLI will check that the image is not complete and clean the cache for you.
|
||||
Check whether the SD card is locked (a physical switch on the side of the card).
|
||||
|
||||
## I get a permission error when burning to an sdcard
|
||||
## I get EINVAL errors on Cygwin
|
||||
|
||||
- The SDCard is locked.
|
||||
|
||||
### I get EINVAL errors on Cygwin
|
||||
|
||||
The errors look something like this:
|
||||
The errors may look something like this:
|
||||
|
||||
```
|
||||
net.js:156
|
||||
this._handle.open(options.fd);
|
||||
^
|
||||
Error: EINVAL, invalid argument
|
||||
at new Socket (net.js:156:18)
|
||||
at process.stdin (node.js:664:19)
|
||||
at Object.Interface.createInterface (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\node_modules\readline2\index.js:31:43)
|
||||
at PromptUI.UI (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\ui\baseUI.js:23:40)
|
||||
at new PromptUI (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\ui\prompt.js:26:8)
|
||||
at Object.promptModule [as prompt] (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\inquirer.js:27:14)
|
||||
```
|
||||
|
||||
- Some interactive widgets don't work on `Cygwin`. If you're running Windows, it's preferrable that you use `cmd.exe`, as `Cygwin` is [not official supported by Node.js](https://github.com/chjj/blessed/issues/56#issuecomment-42671945).
|
||||
Some interactive widgets don't work on `Cygwin`. On Windows, PowerShell or `cmd.exe` are better
|
||||
supported. Alternative shells are [listed in the README
|
||||
file](./README.md#choosing-a-shell-command-promptterminal).
|
||||
|
||||
## I get `Invalid MBR boot signature` when configuring a device
|
||||
|
||||
@ -76,7 +64,9 @@ Or in Windows:
|
||||
|
||||
## I get `EACCES: permission denied` when logging in
|
||||
|
||||
The balena CLI stores the session token in `$HOME/.balena` or `C:\Users\<user>\_balena` in UNIX based operating systems and Windows respectively. This error usually indicates that the user doesn't have permissions over that directory, which can happen if you ran the balena CLI as `root`, and thus the directory got owned by him.
|
||||
The balena CLI stores the session token in `$HOME/.balena` or `C:\Users\<user>\_balena` in UNIX based
|
||||
operating systems and Windows respectively. This error usually indicates that the user doesn't have
|
||||
permissions over that directory, which can happen if the CLI was executed as the `root` user.
|
||||
|
||||
Try resetting the ownership by running:
|
||||
|
||||
@ -86,7 +76,15 @@ $ sudo chown -R <user> $HOME/.balena
|
||||
|
||||
## Broken line wrapping / cursor behavior with `balena ssh`
|
||||
|
||||
Users sometimes come across broken line wrapping or cursor behavior in text terminals, for example when long command lines are typed in a `balena ssh` session, or when using text editors like `vim` or `nano`. This is not something specific to the balena CLI, being also a commonly reported issue with standard remote terminal tools like `ssh` or `telnet`. It is often a remote shell configuration issue (files like `/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile` and the like), including UTF-8 misconfiguration, the use of unsupported ASCII control characters in shell prompt formatting (e.g. the `$PS1` env var) or the output of tools or log files that use colored text. The issue can sometimes be fixed by resizing the client terminal window, or by running one or more of the following commands on the shell:
|
||||
Users sometimes come across broken line wrapping or cursor behavior in text terminals, for example
|
||||
when long command lines are typed in a `balena ssh` session, or when using text editors like `vim`
|
||||
or `nano`. This is not something specific to the balena CLI, being also a commonly reported issue
|
||||
with standard remote terminal tools like `ssh` or `telnet`. It is often a remote shell
|
||||
configuration issue (files like `/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile`
|
||||
and the like on the remote machine), including UTF-8 misconfiguration, the use of unsupported ASCII
|
||||
control characters in shell prompt formatting (e.g. the `$PS1` env var) or the output of tools or
|
||||
log files that use colored text. The issue can sometimes be fixed by simply resizing the client
|
||||
terminal window, or by running one or more of the following commands on the shell:
|
||||
|
||||
```sh
|
||||
export TERMINAL=linux
|
||||
@ -112,10 +110,10 @@ If nothing seems to help, consider also using a different client-side terminal a
|
||||
## "Docker seems to be unavailable" error when using Windows Subsystem for Linux (WSL)
|
||||
|
||||
When running on WSL, the recommendation is to install a CLI release for Linux, like the standalone
|
||||
zip package for Linux. However, commands like "balena build" that contact a local Docker daemon,
|
||||
like the Docker Desktop for Windows, will try to reach Docker at the Unix socket path
|
||||
`/var/run/docker.sock`, while Docker Desktop for Windows uses a Windows named pipe at
|
||||
`//./pipe/docker_engine` (which the Linux CLI on WSL cannot use). A solution is:
|
||||
zip package for Linux. However, commands like "balena build" will, by default, attempt to reach the
|
||||
Docker daemon at the Unix socket path `/var/run/docker.sock`, while Docker Desktop for Windows uses
|
||||
a Windows named pipe at `//./pipe/docker_engine` (which the Linux CLI on WSL cannot use). A
|
||||
solution is:
|
||||
|
||||
- Open the Docker Desktop for Windows settings panel and tick the checkbox _"Expose daemon on tcp://localhost:2375 without TLS"._
|
||||
- On the WSL command line, set an env var:
|
||||
|
@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { JsonVersions } from '../lib/actions-oclif/version';
|
||||
import type { JsonVersions } from '../lib/commands/version';
|
||||
|
||||
import { run as oclifRun } from '@oclif/dev-cli';
|
||||
import * as archiver from 'archiver';
|
||||
@ -409,3 +409,12 @@ export async function catchUncommitted(): Promise<void> {
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function testShrinkwrap(): Promise<void> {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] platform=${process.platform}`);
|
||||
}
|
||||
if (process.platform !== 'win32') {
|
||||
await whichSpawn(path.resolve(__dirname, 'test-lock-deduplicated.sh'));
|
||||
}
|
||||
}
|
||||
|
@ -26,145 +26,153 @@ import { MarkdownFileParser } from './utils';
|
||||
* some content to this object.
|
||||
*/
|
||||
const capitanoDoc = {
|
||||
title: 'Balena CLI Documentation',
|
||||
title: 'balena CLI Documentation',
|
||||
introduction: '',
|
||||
categories: [
|
||||
{
|
||||
title: 'API keys',
|
||||
files: ['build/actions-oclif/api-key/generate.js'],
|
||||
files: ['build/commands/api-key/generate.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
files: [
|
||||
'build/actions-oclif/apps.js',
|
||||
'build/actions-oclif/app/index.js',
|
||||
'build/actions-oclif/app/create.js',
|
||||
'build/actions-oclif/app/rm.js',
|
||||
'build/actions-oclif/app/restart.js',
|
||||
'build/commands/apps.js',
|
||||
'build/commands/app/index.js',
|
||||
'build/commands/app/create.js',
|
||||
'build/commands/app/purge.js',
|
||||
'build/commands/app/rename.js',
|
||||
'build/commands/app/restart.js',
|
||||
'build/commands/app/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: [
|
||||
'build/actions-oclif/login.js',
|
||||
'build/actions-oclif/logout.js',
|
||||
'build/actions-oclif/whoami.js',
|
||||
'build/commands/login.js',
|
||||
'build/commands/logout.js',
|
||||
'build/commands/whoami.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: [
|
||||
'build/actions-oclif/device/identify.js',
|
||||
'build/actions-oclif/device/init.js',
|
||||
'build/actions-oclif/device/index.js',
|
||||
'build/actions-oclif/device/move.js',
|
||||
'build/actions-oclif/device/reboot.js',
|
||||
'build/actions-oclif/device/register.js',
|
||||
'build/actions-oclif/device/rename.js',
|
||||
'build/actions-oclif/device/rm.js',
|
||||
'build/actions-oclif/device/shutdown.js',
|
||||
'build/actions-oclif/devices/index.js',
|
||||
'build/actions-oclif/devices/supported.js',
|
||||
'build/actions-oclif/device/os-update.js',
|
||||
'build/actions-oclif/device/public-url.js',
|
||||
'build/commands/devices/index.js',
|
||||
'build/commands/devices/supported.js',
|
||||
'build/commands/device/index.js',
|
||||
'build/commands/device/identify.js',
|
||||
'build/commands/device/init.js',
|
||||
'build/commands/device/move.js',
|
||||
'build/commands/device/os-update.js',
|
||||
'build/commands/device/public-url.js',
|
||||
'build/commands/device/purge.js',
|
||||
'build/commands/device/reboot.js',
|
||||
'build/commands/device/register.js',
|
||||
'build/commands/device/rename.js',
|
||||
'build/commands/device/restart.js',
|
||||
'build/commands/device/rm.js',
|
||||
'build/commands/device/shutdown.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [
|
||||
'build/actions-oclif/envs.js',
|
||||
'build/actions-oclif/env/add.js',
|
||||
'build/actions-oclif/env/rename.js',
|
||||
'build/actions-oclif/env/rm.js',
|
||||
'build/commands/envs.js',
|
||||
'build/commands/env/add.js',
|
||||
'build/commands/env/rename.js',
|
||||
'build/commands/env/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
files: [
|
||||
'build/actions-oclif/tags.js',
|
||||
'build/actions-oclif/tag/rm.js',
|
||||
'build/actions-oclif/tag/set.js',
|
||||
'build/commands/tags.js',
|
||||
'build/commands/tag/rm.js',
|
||||
'build/commands/tag/set.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Help and Version',
|
||||
files: ['build/actions/help.js', 'build/actions-oclif/version.js'],
|
||||
files: ['help', 'build/commands/version.js'],
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: [
|
||||
'build/actions-oclif/keys.js',
|
||||
'build/actions-oclif/key/index.js',
|
||||
'build/actions-oclif/key/add.js',
|
||||
'build/actions-oclif/key/rm.js',
|
||||
'build/commands/keys.js',
|
||||
'build/commands/key/index.js',
|
||||
'build/commands/key/add.js',
|
||||
'build/commands/key/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: ['build/actions-oclif/logs.js'],
|
||||
files: ['build/commands/logs.js'],
|
||||
},
|
||||
{
|
||||
title: 'Network',
|
||||
files: [
|
||||
'build/actions-oclif/scan.js',
|
||||
'build/actions-oclif/ssh.js',
|
||||
'build/actions-oclif/tunnel.js',
|
||||
'build/commands/scan.js',
|
||||
'build/commands/ssh.js',
|
||||
'build/commands/tunnel.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: ['build/actions-oclif/note.js'],
|
||||
files: ['build/commands/note.js'],
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: [
|
||||
'build/actions-oclif/os/build-config.js',
|
||||
'build/actions-oclif/os/configure.js',
|
||||
'build/actions-oclif/os/versions.js',
|
||||
'build/actions-oclif/os/download.js',
|
||||
'build/actions-oclif/os/initialize.js',
|
||||
'build/commands/os/build-config.js',
|
||||
'build/commands/os/configure.js',
|
||||
'build/commands/os/versions.js',
|
||||
'build/commands/os/download.js',
|
||||
'build/commands/os/initialize.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: [
|
||||
'build/actions-oclif/config/generate.js',
|
||||
'build/actions-oclif/config/inject.js',
|
||||
'build/actions-oclif/config/read.js',
|
||||
'build/actions-oclif/config/reconfigure.js',
|
||||
'build/actions-oclif/config/write.js',
|
||||
'build/commands/config/generate.js',
|
||||
'build/commands/config/inject.js',
|
||||
'build/commands/config/read.js',
|
||||
'build/commands/config/reconfigure.js',
|
||||
'build/commands/config/write.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: ['build/actions/preload.js'],
|
||||
files: ['build/commands/preload.js'],
|
||||
},
|
||||
{
|
||||
title: 'Push',
|
||||
files: ['build/actions-oclif/push.js'],
|
||||
files: ['build/commands/push.js'],
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: ['build/actions-oclif/settings.js'],
|
||||
files: ['build/commands/settings.js'],
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
files: [
|
||||
'build/actions-oclif/local/configure.js',
|
||||
'build/actions-oclif/local/flash.js',
|
||||
'build/commands/local/configure.js',
|
||||
'build/commands/local/flash.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
files: ['build/actions-oclif/build.js', 'build/actions/deploy.js'],
|
||||
files: ['build/commands/build.js', 'build/commands/deploy.js'],
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
files: ['build/actions-oclif/join.js', 'build/actions-oclif/leave.js'],
|
||||
files: ['build/commands/join.js', 'build/commands/leave.js'],
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
files: ['build/actions-oclif/util/available-drives.js'],
|
||||
files: ['build/commands/util/available-drives.js'],
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
files: ['build/commands/support.js'],
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -191,7 +199,9 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
|
||||
return match && match[2];
|
||||
}),
|
||||
mdParser.getSectionOfTitle('Installation'),
|
||||
mdParser.getSectionOfTitle('Getting Started'),
|
||||
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),
|
||||
mdParser.getSectionOfTitle('Logging in'),
|
||||
mdParser.getSectionOfTitle('Proxy support'),
|
||||
mdParser.getSectionOfTitle('Support, FAQ and troubleshooting'),
|
||||
mdParser.getSectionOfTitle('Deprecation policy'),
|
||||
]);
|
||||
|
5
automation/capitanodoc/doc-types.d.ts
vendored
5
automation/capitanodoc/doc-types.d.ts
vendored
@ -15,7 +15,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command as OclifCommandClass } from '@oclif/command';
|
||||
import { CommandDefinition as CapitanoCommand } from 'capitano';
|
||||
|
||||
type OclifCommand = typeof OclifCommandClass;
|
||||
|
||||
@ -27,7 +26,7 @@ export interface Document {
|
||||
|
||||
export interface Category {
|
||||
title: string;
|
||||
commands: Array<CapitanoCommand | OclifCommand>;
|
||||
commands: OclifCommand[];
|
||||
}
|
||||
|
||||
export { CapitanoCommand, OclifCommand };
|
||||
export { OclifCommand };
|
||||
|
@ -14,12 +14,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
import { getCapitanoDoc } from './capitanodoc';
|
||||
import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
|
||||
import { Category, Document, OclifCommand } from './doc-types';
|
||||
import * as markdown from './markdown';
|
||||
import { stripIndent } from '../../lib/utils/lazy';
|
||||
|
||||
/**
|
||||
* Generates the markdown document (as a string) for the CLI documentation
|
||||
@ -40,11 +39,7 @@ export async function renderMarkdown(): Promise<string> {
|
||||
};
|
||||
|
||||
for (const jsFilename of commandCategory.files) {
|
||||
category.commands.push(
|
||||
...(jsFilename.includes('actions-oclif')
|
||||
? importOclifCommands(jsFilename)
|
||||
: importCapitanoCommands(jsFilename)),
|
||||
);
|
||||
category.commands.push(...importOclifCommands(jsFilename));
|
||||
}
|
||||
result.categories.push(category);
|
||||
}
|
||||
@ -52,27 +47,48 @@ export async function renderMarkdown(): Promise<string> {
|
||||
return markdown.render(result);
|
||||
}
|
||||
|
||||
function importCapitanoCommands(jsFilename: string): CapitanoCommand[] {
|
||||
const actions = require(path.join(process.cwd(), jsFilename));
|
||||
const commands: CapitanoCommand[] = [];
|
||||
// Help is now managed via a plugin
|
||||
// This fake command allows capitanodoc to include help in docs
|
||||
class FakeHelpCommand {
|
||||
description = stripIndent`
|
||||
List balena commands, or get detailed help for a specific command.
|
||||
|
||||
if (actions.signature) {
|
||||
commands.push(_.omit(actions, 'action') as any);
|
||||
} else {
|
||||
for (const actionName of Object.keys(actions)) {
|
||||
const actionCommand = actions[actionName];
|
||||
commands.push(_.omit(actionCommand, 'action') as any);
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
List balena commands, or get detailed help for a specific command.
|
||||
`;
|
||||
|
||||
examples = [
|
||||
'$ balena help',
|
||||
'$ balena help apps',
|
||||
'$ balena help os download',
|
||||
];
|
||||
|
||||
args = [
|
||||
{
|
||||
name: 'command',
|
||||
description: 'command to show help for',
|
||||
},
|
||||
];
|
||||
|
||||
usage = 'help [command]';
|
||||
|
||||
flags = {
|
||||
verbose: {
|
||||
description: 'show additional commands',
|
||||
char: '-v',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
// TODO: Currently oclif commands with no `usage` overridden will cause
|
||||
// an error when parsed. This should be improved so that `usage` does not have
|
||||
// to be overridden if not necessary.
|
||||
const command: OclifCommand = require(path.join(process.cwd(), jsFilename))
|
||||
.default as OclifCommand;
|
||||
|
||||
const command: OclifCommand =
|
||||
jsFilename === 'help'
|
||||
? ((new FakeHelpCommand() as unknown) as OclifCommand)
|
||||
: (require(path.join(process.cwd(), jsFilename)).default as OclifCommand);
|
||||
|
||||
return [command];
|
||||
}
|
||||
|
||||
|
@ -20,33 +20,10 @@ import * as _ from 'lodash';
|
||||
|
||||
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
|
||||
import { capitanoizeOclifUsage } from '../../lib/utils/oclif-utils';
|
||||
import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
|
||||
import * as utils from './utils';
|
||||
|
||||
function renderCapitanoCommand(command: CapitanoCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.signature)}`, command.help!];
|
||||
|
||||
if (!_.isEmpty(command.options)) {
|
||||
result.push('### Options');
|
||||
|
||||
for (const option of command.options!) {
|
||||
if (option == null) {
|
||||
throw new Error(`Undefined option in markdown generation!`);
|
||||
}
|
||||
if (option.description == null) {
|
||||
throw new Error(`Undefined option.description in markdown generation!`);
|
||||
}
|
||||
result.push(
|
||||
`#### ${utils.parseCapitanoOption(option)}`,
|
||||
option.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
import { Category, Document, OclifCommand } from './doc-types';
|
||||
|
||||
function renderOclifCommand(command: OclifCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.usage)}`];
|
||||
const result = [`## ${ent.encode(command.usage || '')}`];
|
||||
const description = (command.description || '')
|
||||
.split('\n')
|
||||
.slice(1) // remove the first line, which oclif uses as help header
|
||||
@ -86,11 +63,7 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
||||
function renderCategory(category: Category): string[] {
|
||||
const result = [`# ${category.title}`];
|
||||
for (const command of category.commands) {
|
||||
result.push(
|
||||
...(typeof command === 'object'
|
||||
? renderCapitanoCommand(command)
|
||||
: renderOclifCommand(command)),
|
||||
);
|
||||
result.push(...renderOclifCommand(command));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -107,10 +80,7 @@ function renderToc(categories: Category[]): string[] {
|
||||
result.push(
|
||||
category.commands
|
||||
.map((command) => {
|
||||
const signature =
|
||||
typeof command === 'object'
|
||||
? command.signature // Capitano
|
||||
: capitanoizeOclifUsage(command.usage); // oclif
|
||||
const signature = capitanoizeOclifUsage(command.usage);
|
||||
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
||||
})
|
||||
.join('\n'),
|
||||
@ -134,12 +104,10 @@ function sortCommands(doc: Document): void {
|
||||
for (const category of doc.categories) {
|
||||
if (category.title in manualCategorySorting) {
|
||||
category.commands = category.commands.sort(
|
||||
getManualSortCompareFunction<CapitanoCommand | OclifCommand, string>(
|
||||
getManualSortCompareFunction<OclifCommand, string>(
|
||||
manualCategorySorting[category.title],
|
||||
(cmd: CapitanoCommand | OclifCommand, x: string) =>
|
||||
typeof cmd === 'object' // Capitano vs oclif command
|
||||
? cmd.signature.replace(/\W+/g, ' ').includes(x)
|
||||
: (cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
|
||||
(cmd: OclifCommand, x: string) =>
|
||||
(cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
buildOclifInstaller,
|
||||
buildStandaloneZip,
|
||||
catchUncommitted,
|
||||
testShrinkwrap,
|
||||
} from './build-bin';
|
||||
import {
|
||||
release,
|
||||
@ -63,6 +64,7 @@ export async function run(args?: string[]) {
|
||||
'build:installer': buildOclifInstaller,
|
||||
'build:standalone': buildStandaloneZip,
|
||||
'catch-uncommitted': catchUncommitted,
|
||||
'test-shrinkwrap': testShrinkwrap,
|
||||
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
release,
|
||||
};
|
||||
|
17
automation/test-lock-deduplicated.sh
Executable file
17
automation/test-lock-deduplicated.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cp npm-shrinkwrap.json npm-shrinkwrap.json.old
|
||||
npm i
|
||||
npm dedupe
|
||||
npm i
|
||||
|
||||
if ! diff -q npm-shrinkwrap.json npm-shrinkwrap.json.old > /dev/null; then
|
||||
rm npm-shrinkwrap.json.old
|
||||
echo "** npm-shrinkwrap.json was not deduplicated or not fully committed - FAIL **";
|
||||
echo "** Please run 'npm ci', followed by 'npm dedupe' **";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
rm npm-shrinkwrap.json.old
|
@ -206,7 +206,7 @@ export async function which(program: string): Promise<string> {
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args: string[],
|
||||
args?: string[],
|
||||
): Promise<void> {
|
||||
const program = await which(programName);
|
||||
let error: Error | undefined;
|
||||
|
@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
@ -10,7 +12,7 @@ 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'
|
||||
cacheFile: __dirname + '/.fast-boot.json',
|
||||
});
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
|
@ -1,14 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// ****************************************************************************
|
||||
// THIS IS FOR DEV PERROSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||
// THIS IS FOR DEV PURPOSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||
// Before opening a PR you should build and test your changes using bin/balena
|
||||
// ****************************************************************************
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
|
||||
// Allow balena-dev to work with oclif by temporarily
|
||||
// pointing oclif config options to lib/ instead of build/
|
||||
modifyOclifPaths();
|
||||
// Undo changes on exit
|
||||
process.on('exit', function () {
|
||||
modifyOclifPaths(true);
|
||||
});
|
||||
// Undo changes in case of ctrl-v
|
||||
process.on('SIGINT', function () {
|
||||
modifyOclifPaths(true);
|
||||
});
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheScope: __dirname + '/..',
|
||||
@ -18,8 +35,6 @@ require('fast-boot2').start({
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
|
||||
// default option. We upgraded ts-node and found that adding 'transpile-only'
|
||||
// was necessary to avoid a mysterious 'null' error message. On the plus side,
|
||||
@ -30,3 +45,30 @@ require('ts-node').register({
|
||||
transpileOnly: true,
|
||||
});
|
||||
require('../lib/app').run();
|
||||
|
||||
// Modify package.json oclif paths from build/ -> lib/, or vice versa
|
||||
function modifyOclifPaths(revert) {
|
||||
const fs = require('fs');
|
||||
const packageJsonPath = path.join(rootDir, 'package.json');
|
||||
|
||||
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
||||
const packageObj = JSON.parse(packageJson);
|
||||
|
||||
if (!packageObj.oclif) {
|
||||
return;
|
||||
}
|
||||
|
||||
let oclifSectionText = JSON.stringify(packageObj.oclif);
|
||||
if (!revert) {
|
||||
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/lib/');
|
||||
} else {
|
||||
oclifSectionText = oclifSectionText.replace(/\/lib\//g, '/build/');
|
||||
}
|
||||
|
||||
packageObj.oclif = JSON.parse(oclifSectionText);
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
`${JSON.stringify(packageObj, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
1048
doc/cli.markdown
1048
doc/cli.markdown
File diff suppressed because it is too large
Load Diff
@ -1,153 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const yes = {
|
||||
signature: 'yes',
|
||||
description: 'confirm non interactively',
|
||||
boolean: true,
|
||||
alias: 'y',
|
||||
};
|
||||
|
||||
export interface YesOption {
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
export const optionalApplication = {
|
||||
signature: 'application',
|
||||
parameter: 'application',
|
||||
description: 'application name',
|
||||
alias: ['a', 'app'],
|
||||
};
|
||||
|
||||
export const application = {
|
||||
...optionalApplication,
|
||||
required: 'You have to specify an application',
|
||||
};
|
||||
|
||||
export const optionalRelease = {
|
||||
signature: 'release',
|
||||
parameter: 'release',
|
||||
description: 'release id',
|
||||
alias: 'r',
|
||||
};
|
||||
|
||||
export const optionalDevice = {
|
||||
signature: 'device',
|
||||
parameter: 'device',
|
||||
description: 'device uuid',
|
||||
alias: 'd',
|
||||
};
|
||||
|
||||
export const optionalDeviceApiKey = {
|
||||
signature: 'deviceApiKey',
|
||||
description:
|
||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||
parameter: 'device-api-key',
|
||||
alias: 'k',
|
||||
};
|
||||
|
||||
export const optionalDeviceType = {
|
||||
signature: 'deviceType',
|
||||
description: 'device type slug',
|
||||
parameter: 'device-type',
|
||||
};
|
||||
|
||||
export const optionalOsVersion = {
|
||||
signature: 'version',
|
||||
description: 'a balenaOS version',
|
||||
parameter: 'version',
|
||||
};
|
||||
|
||||
export type OptionalOsVersionOption = Partial<OsVersionOption>;
|
||||
|
||||
export const osVersion = {
|
||||
...exports.optionalOsVersion,
|
||||
required: 'You have to specify an exact os version',
|
||||
};
|
||||
|
||||
export interface OsVersionOption {
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export const booleanDevice = {
|
||||
signature: 'device',
|
||||
description: 'device',
|
||||
boolean: true,
|
||||
alias: 'd',
|
||||
};
|
||||
|
||||
export const osVersionOrSemver = {
|
||||
signature: 'version',
|
||||
description: `\
|
||||
exact version number, or a valid semver range,
|
||||
or 'latest' (includes pre-releases),
|
||||
or 'default' (excludes pre-releases if at least one stable version is available),
|
||||
or 'recommended' (excludes pre-releases, will fail if only pre-release versions are available),
|
||||
or 'menu' (will show the interactive menu)\
|
||||
`,
|
||||
parameter: 'version',
|
||||
};
|
||||
|
||||
export const network = {
|
||||
signature: 'network',
|
||||
parameter: 'network',
|
||||
description: 'network type',
|
||||
alias: 'n',
|
||||
};
|
||||
|
||||
export const wifiSsid = {
|
||||
signature: 'ssid',
|
||||
parameter: 'ssid',
|
||||
description: 'wifi ssid, if network is wifi',
|
||||
alias: 's',
|
||||
};
|
||||
|
||||
export const wifiKey = {
|
||||
signature: 'key',
|
||||
parameter: 'key',
|
||||
description: 'wifi key, if network is wifi',
|
||||
alias: 'k',
|
||||
};
|
||||
|
||||
export const forceUpdateLock = {
|
||||
signature: 'force',
|
||||
description: 'force action if the update lock is set',
|
||||
boolean: true,
|
||||
alias: 'f',
|
||||
};
|
||||
|
||||
export const drive = {
|
||||
signature: 'drive',
|
||||
description: `the drive to write the image to, like \`/dev/sdb\` or \`/dev/mmcblk0\`. \
|
||||
Careful with this as you can erase your hard drive. \
|
||||
Check \`balena util available-drives\` for available options.`,
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
};
|
||||
|
||||
export const advancedConfig = {
|
||||
signature: 'advanced',
|
||||
description: 'show advanced configuration options',
|
||||
boolean: true,
|
||||
alias: 'v',
|
||||
};
|
||||
|
||||
export const hostOSAccess = {
|
||||
signature: 'host',
|
||||
boolean: true,
|
||||
description: 'access host OS (for devices with balenaOS >= 2.0.0+rev1)',
|
||||
alias: 's',
|
||||
};
|
@ -1,318 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as dockerUtils from '../utils/docker';
|
||||
import * as compose from '../utils/compose';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||
|
||||
/**
|
||||
* Opts must be an object with the following keys:
|
||||
* app: the application instance to deploy to
|
||||
* image: the image to deploy; optional
|
||||
* dockerfilePath: name of an alternative Dockerfile; optional
|
||||
* shouldPerformBuild
|
||||
* shouldUploadLogs
|
||||
* buildEmulated
|
||||
* buildOpts: arguments to forward to docker build command
|
||||
*
|
||||
* @param {any} docker
|
||||
* @param {import('../utils/logger')} logger
|
||||
* @param {import('../utils/compose-types').ComposeOpts} composeOpts
|
||||
* @param {any} opts
|
||||
*/
|
||||
const deployProject = function (docker, logger, composeOpts, opts) {
|
||||
const Bluebird = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const doodles = require('resin-doodles');
|
||||
const sdk = getBalenaSdk();
|
||||
const {
|
||||
deployProject: $deployProject,
|
||||
loadProject,
|
||||
} = require('../utils/compose_ts');
|
||||
|
||||
return loadProject(logger, composeOpts, opts.image)
|
||||
.then(function (project) {
|
||||
if (
|
||||
project.descriptors.length > 1 &&
|
||||
!opts.app.application_type?.[0]?.supports_multicontainer
|
||||
) {
|
||||
throw new ExpectedError(
|
||||
'Target application does not support multiple containers. Aborting!',
|
||||
);
|
||||
}
|
||||
|
||||
// find which services use images that already exist locally
|
||||
return (
|
||||
Bluebird.map(project.descriptors, function (d) {
|
||||
// unconditionally build (or pull) if explicitly requested
|
||||
if (opts.shouldPerformBuild) {
|
||||
return d;
|
||||
}
|
||||
return docker
|
||||
.getImage(typeof d.image === 'string' ? d.image : d.image.tag)
|
||||
.inspect()
|
||||
.return(d.serviceName)
|
||||
.catchReturn();
|
||||
})
|
||||
.filter((d) => !!d)
|
||||
.then(function (servicesToSkip) {
|
||||
// multibuild takes in a composition and always attempts to
|
||||
// build or pull all services. we workaround that here by
|
||||
// passing a modified composition.
|
||||
const compositionToBuild = _.cloneDeep(project.composition);
|
||||
compositionToBuild.services = _.omit(
|
||||
compositionToBuild.services,
|
||||
servicesToSkip,
|
||||
);
|
||||
if (_.size(compositionToBuild.services) === 0) {
|
||||
logger.logInfo(
|
||||
'Everything is up to date (use --build to force a rebuild)',
|
||||
);
|
||||
return {};
|
||||
}
|
||||
return compose
|
||||
.buildProject(
|
||||
docker,
|
||||
logger,
|
||||
project.path,
|
||||
project.name,
|
||||
compositionToBuild,
|
||||
opts.app.arch,
|
||||
opts.app.is_for__device_type[0].slug,
|
||||
opts.buildEmulated,
|
||||
opts.buildOpts,
|
||||
composeOpts.inlineLogs,
|
||||
composeOpts.convertEol,
|
||||
composeOpts.dockerfilePath,
|
||||
composeOpts.nogitignore,
|
||||
composeOpts.multiDockerignore,
|
||||
)
|
||||
.then((builtImages) => _.keyBy(builtImages, 'serviceName'));
|
||||
})
|
||||
.then((builtImages) =>
|
||||
project.descriptors.map(
|
||||
(d) =>
|
||||
builtImages[d.serviceName] ?? {
|
||||
serviceName: d.serviceName,
|
||||
name: typeof d.image === 'string' ? d.image : d.image.tag,
|
||||
logs: 'Build skipped; image for service already exists.',
|
||||
props: {},
|
||||
},
|
||||
),
|
||||
)
|
||||
// @ts-ignore slightly different return types of partial vs non-partial release
|
||||
.then(function (images) {
|
||||
if (opts.app.application_type?.[0]?.is_legacy) {
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
|
||||
const msg = getChalk().yellow(
|
||||
'Target application requires legacy deploy method.',
|
||||
);
|
||||
logger.logWarn(msg);
|
||||
|
||||
return Bluebird.join(
|
||||
docker,
|
||||
logger,
|
||||
sdk.auth.getToken(),
|
||||
sdk.auth.whoami(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
{
|
||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
appName: opts.appName,
|
||||
imageName: images[0].name,
|
||||
buildLogs: images[0].logs,
|
||||
shouldUploadLogs: opts.shouldUploadLogs,
|
||||
},
|
||||
deployLegacy,
|
||||
).then((releaseId) =>
|
||||
// @ts-ignore releaseId should be inferred as a number because that's what deployLegacy is
|
||||
// typed as returning but the .js type-checking doesn't manage to infer it correctly due to
|
||||
// Promise.join typings
|
||||
sdk.models.release.get(releaseId, { $select: ['commit'] }),
|
||||
);
|
||||
}
|
||||
return Promise.all([
|
||||
sdk.auth.getUserId(),
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('apiUrl'),
|
||||
]).then(([userId, auth, apiEndpoint]) =>
|
||||
$deployProject(
|
||||
docker,
|
||||
logger,
|
||||
project.composition,
|
||||
images,
|
||||
opts.app.id,
|
||||
userId,
|
||||
`Bearer ${auth}`,
|
||||
apiEndpoint,
|
||||
!opts.shouldUploadLogs,
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function (release) {
|
||||
logger.outputDeferredMessages();
|
||||
logger.logSuccess('Deploy succeeded!');
|
||||
logger.logSuccess(`Release: ${release.commit}`);
|
||||
console.log();
|
||||
console.log(doodles.getDoodle()); // Show charlie
|
||||
console.log();
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.logError('Deploy failed');
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
export const deploy = {
|
||||
signature: 'deploy <appName> [image]',
|
||||
description:
|
||||
'Deploy a single image or a multicontainer project to a balena application',
|
||||
help: `\
|
||||
Usage: \`deploy <appName> ([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
|
||||
(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.)
|
||||
|
||||
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.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
\`balena deploy <appOwnerUsername>/<appName>\`.
|
||||
|
||||
When --build is used, all options supported by \`balena build\` are also supported
|
||||
by this command.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
${dockerignoreHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena deploy myApp
|
||||
$ balena deploy myApp --build --source myBuildDir/
|
||||
$ balena deploy myApp myApp/myImage\
|
||||
`,
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
options: dockerUtils.appendOptions(
|
||||
compose.appendOptions([
|
||||
{
|
||||
signature: 'source',
|
||||
parameter: 'source',
|
||||
description:
|
||||
'Specify an alternate source directory; default is the working directory',
|
||||
alias: 's',
|
||||
},
|
||||
{
|
||||
signature: 'build',
|
||||
boolean: true,
|
||||
description: 'Force a rebuild before deploy',
|
||||
alias: 'b',
|
||||
},
|
||||
{
|
||||
signature: 'nologupload',
|
||||
description:
|
||||
"Don't upload build logs to the dashboard with image (if building)",
|
||||
boolean: true,
|
||||
},
|
||||
]),
|
||||
),
|
||||
async action(params, options) {
|
||||
// compositions with many services trigger misleading warnings
|
||||
// @ts-ignore editing property that isn't typed but does exist
|
||||
require('events').defaultMaxListeners = 1000;
|
||||
const sdk = getBalenaSdk();
|
||||
const {
|
||||
getRegistrySecrets,
|
||||
validateProjectDirectory,
|
||||
} = require('../utils/compose_ts');
|
||||
const helpers = require('../utils/helpers');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
// when Capitano converts a positional parameter (but not an option)
|
||||
// to a number, the original value is preserved with the _raw suffix
|
||||
let { appName, appName_raw, image } = params;
|
||||
|
||||
// look into "balena build" options if appName isn't given
|
||||
appName = appName_raw || appName || options.application;
|
||||
delete options.application;
|
||||
|
||||
if (appName == null) {
|
||||
throw new ExpectedError(
|
||||
'Please specify the name of the application to deploy',
|
||||
);
|
||||
}
|
||||
|
||||
if (image != null && options.build) {
|
||||
throw new ExpectedError(
|
||||
'Build option is not applicable when specifying an image',
|
||||
);
|
||||
}
|
||||
|
||||
if (image) {
|
||||
const registrySecrets = await getRegistrySecrets(
|
||||
sdk,
|
||||
options['registry-secrets'],
|
||||
);
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
} else {
|
||||
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 app = await helpers.getAppWithArch(appName);
|
||||
|
||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||
dockerUtils.getDocker(options),
|
||||
dockerUtils.generateBuildOpts(options),
|
||||
compose.generateOpts(options),
|
||||
]);
|
||||
await deployProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
appName, // may be prefixed by 'owner/', unlike app.app_name
|
||||
image,
|
||||
shouldPerformBuild: !!options.build,
|
||||
shouldUploadLogs: !options.nologupload,
|
||||
buildEmulated: !!options.emulated,
|
||||
buildOpts,
|
||||
});
|
||||
},
|
||||
};
|
@ -1,190 +0,0 @@
|
||||
/*
|
||||
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 * as _ from 'lodash';
|
||||
|
||||
import * as capitano from 'capitano';
|
||||
import * as columnify from 'columnify';
|
||||
import * as messages from '../utils/messages';
|
||||
import { getManualSortCompareFunction } from '../utils/helpers';
|
||||
import { exitWithExpectedError } from '../errors';
|
||||
import { getOclifHelpLinePairs } from './help_ts';
|
||||
|
||||
const parse = (object) =>
|
||||
_.map(object, function (item) {
|
||||
// Hacky way to determine if an object is
|
||||
// a function or a command
|
||||
let signature;
|
||||
if (item.alias != null) {
|
||||
signature = item.toString();
|
||||
} else {
|
||||
signature = item.signature.toString();
|
||||
}
|
||||
|
||||
return [signature, item.description];
|
||||
});
|
||||
|
||||
const indent = function (text) {
|
||||
text = _.map(text.split('\n'), (line) => ' ' + line);
|
||||
return text.join('\n');
|
||||
};
|
||||
|
||||
const print = (usageDescriptionPairs) =>
|
||||
console.log(
|
||||
indent(
|
||||
columnify(_.fromPairs(usageDescriptionPairs), {
|
||||
showHeaders: false,
|
||||
minWidth: 35,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const manuallySortedPrimaryCommands = [
|
||||
'help',
|
||||
'login',
|
||||
'push',
|
||||
'logs',
|
||||
'ssh',
|
||||
'apps',
|
||||
'app',
|
||||
'devices',
|
||||
'device',
|
||||
'tunnel',
|
||||
'preload',
|
||||
'build',
|
||||
'deploy',
|
||||
'join',
|
||||
'leave',
|
||||
'local scan',
|
||||
];
|
||||
|
||||
const general = function (_params, options, done) {
|
||||
console.log('Usage: balena [COMMAND] [OPTIONS]\n');
|
||||
|
||||
console.log('Primary commands:\n');
|
||||
|
||||
// We do not want the wildcard command
|
||||
// to be printed in the help screen.
|
||||
const commands = capitano.state.commands.filter(
|
||||
(command) => !command.hidden && !command.isWildcard(),
|
||||
);
|
||||
|
||||
const capitanoCommands = _.groupBy(commands, function (command) {
|
||||
if (command.primary) {
|
||||
return 'primary';
|
||||
}
|
||||
return 'secondary';
|
||||
});
|
||||
|
||||
return getOclifHelpLinePairs()
|
||||
.then(function (oclifHelpLinePairs) {
|
||||
const primaryHelpLinePairs = parse(capitanoCommands.primary)
|
||||
.concat(oclifHelpLinePairs.primary)
|
||||
.sort(
|
||||
getManualSortCompareFunction(manuallySortedPrimaryCommands, function (
|
||||
[signature],
|
||||
manualItem,
|
||||
) {
|
||||
return (
|
||||
signature === manualItem || signature.startsWith(`${manualItem} `)
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const secondaryHelpLinePairs = parse(capitanoCommands.secondary)
|
||||
.concat(oclifHelpLinePairs.secondary)
|
||||
.sort();
|
||||
|
||||
print(primaryHelpLinePairs);
|
||||
|
||||
if (options.verbose) {
|
||||
console.log('\nAdditional commands:\n');
|
||||
print(secondaryHelpLinePairs);
|
||||
} else {
|
||||
console.log(
|
||||
'\nRun `balena help --verbose` to list additional commands',
|
||||
);
|
||||
}
|
||||
|
||||
if (!_.isEmpty(capitano.state.globalOptions)) {
|
||||
console.log('\nGlobal Options:\n');
|
||||
print(parse(capitano.state.globalOptions).sort());
|
||||
}
|
||||
console.log(indent('--debug\n'));
|
||||
|
||||
console.log(messages.help);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(done);
|
||||
};
|
||||
|
||||
const commandHelp = (params, _options, done) =>
|
||||
capitano.state.getMatchCommand(params.command, function (error, command) {
|
||||
if (error != null) {
|
||||
return done(error);
|
||||
}
|
||||
|
||||
if (command == null || command.isWildcard()) {
|
||||
exitWithExpectedError(`Command not found: ${params.command}`);
|
||||
}
|
||||
|
||||
console.log(`Usage: ${command.signature}`);
|
||||
|
||||
if (command.help != null) {
|
||||
console.log(`\n${command.help}`);
|
||||
} else if (command.description != null) {
|
||||
console.log(`\n${_.capitalize(command.description)}`);
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.options)) {
|
||||
console.log('\nOptions:\n');
|
||||
print(parse(command.options).sort());
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
return done();
|
||||
});
|
||||
|
||||
export const help = {
|
||||
signature: 'help [command...]',
|
||||
description: 'show help',
|
||||
help: `\
|
||||
Get detailed help for an specific command.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena help apps
|
||||
$ balena help os download\
|
||||
`,
|
||||
primary: true,
|
||||
options: [
|
||||
{
|
||||
signature: 'verbose',
|
||||
description: 'show additional commands',
|
||||
boolean: true,
|
||||
alias: 'v',
|
||||
},
|
||||
],
|
||||
action(params, options, done) {
|
||||
if (params.command != null) {
|
||||
return commandHelp(params, options, done);
|
||||
} else {
|
||||
return general(params, options, done);
|
||||
}
|
||||
},
|
||||
};
|
@ -1,65 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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 * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import Command from '../command';
|
||||
|
||||
import { capitanoizeOclifUsage } from '../utils/oclif-utils';
|
||||
|
||||
export async function getOclifHelpLinePairs() {
|
||||
const { convertedCommands } = await import('../preparser');
|
||||
const primary: Array<[string, string]> = [];
|
||||
const secondary: Array<[string, string]> = [];
|
||||
|
||||
for (const convertedCmd of convertedCommands) {
|
||||
const [topic, cmd] = convertedCmd.split(':');
|
||||
const pathComponents = ['..', 'actions-oclif', topic];
|
||||
if (cmd) {
|
||||
pathComponents.push(cmd);
|
||||
}
|
||||
|
||||
const cmdModule = await import(path.join(...pathComponents));
|
||||
const command: typeof Command = cmdModule.default;
|
||||
|
||||
if (!command.hidden) {
|
||||
if (command.primary) {
|
||||
primary.push(getCmdUsageDescriptionLinePair(command));
|
||||
} else {
|
||||
secondary.push(getCmdUsageDescriptionLinePair(command));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { primary, secondary };
|
||||
}
|
||||
|
||||
function getCmdUsageDescriptionLinePair(cmd: typeof Command): [string, string] {
|
||||
const usage = capitanoizeOclifUsage(cmd.usage);
|
||||
let description = '';
|
||||
// note: [^] matches any characters (including line breaks), achieving the
|
||||
// same effect as the 's' regex flag which is only supported by Node 9+
|
||||
const matches = /\s*([^]+?)\n[^]*/.exec(cmd.description || '');
|
||||
if (matches && matches.length > 1) {
|
||||
description = _.trimEnd(matches[1], '.');
|
||||
// Only do .lowerFirst() if the second char is not uppercase (e.g. for 'SSH');
|
||||
if (description[1] !== description[1]?.toUpperCase()) {
|
||||
description = _.lowerFirst(description);
|
||||
}
|
||||
}
|
||||
return [usage, description];
|
||||
}
|
@ -1,471 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { getBalenaSdk, getVisuals, getCliForm } from '../utils/lazy';
|
||||
import * as dockerUtils from '../utils/docker';
|
||||
|
||||
const isCurrent = (commit) => commit === 'latest' || commit === 'current';
|
||||
|
||||
/** @type {any} */
|
||||
const applicationExpandOptions = {
|
||||
owns__release: {
|
||||
$select: ['id', 'commit', 'end_timestamp', 'composition'],
|
||||
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
|
||||
$expand: {
|
||||
contains__image: {
|
||||
$select: ['image'],
|
||||
$expand: {
|
||||
image: {
|
||||
$select: ['image_size', 'is_stored_at__image_location'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
$filter: {
|
||||
status: 'success',
|
||||
},
|
||||
},
|
||||
should_be_running__release: {
|
||||
$select: 'commit',
|
||||
},
|
||||
};
|
||||
|
||||
let allDeviceTypes;
|
||||
const getDeviceTypes = async function () {
|
||||
if (allDeviceTypes !== undefined) {
|
||||
return allDeviceTypes;
|
||||
}
|
||||
const balena = getBalenaSdk();
|
||||
return balena.models.config
|
||||
.getDeviceTypes()
|
||||
.then((deviceTypes) => _.sortBy(deviceTypes, 'name'))
|
||||
.tap((dt) => {
|
||||
allDeviceTypes = dt;
|
||||
});
|
||||
};
|
||||
|
||||
const getDeviceTypesWithSameArch = function (deviceTypeSlug) {
|
||||
return getDeviceTypes().then(function (deviceTypes) {
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
||||
const getApplicationsWithSuccessfulBuilds = function (deviceType) {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return getDeviceTypesWithSameArch(deviceType).then((deviceTypes) => {
|
||||
/** @type {import('balena-sdk').PineOptions<ApplicationWithDeviceType & { should_be_running__release: [import('balena-sdk').Release?] }>} */
|
||||
const options = {
|
||||
$filter: {
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: {
|
||||
dt: {
|
||||
slug: { $in: deviceTypes },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
owns__release: {
|
||||
$any: {
|
||||
$alias: 'r',
|
||||
$expr: {
|
||||
r: {
|
||||
status: 'success',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
$expand: applicationExpandOptions,
|
||||
$select: ['id', 'app_name', 'should_track_latest_release'],
|
||||
$orderby: 'app_name asc',
|
||||
};
|
||||
return balena.pine.get({
|
||||
resource: 'my_application',
|
||||
options,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const selectApplication = function (deviceType) {
|
||||
const visuals = getVisuals();
|
||||
const { exitWithExpectedError } = require('../errors');
|
||||
|
||||
const applicationInfoSpinner = new visuals.Spinner(
|
||||
'Downloading list of applications and releases.',
|
||||
);
|
||||
applicationInfoSpinner.start();
|
||||
|
||||
return getApplicationsWithSuccessfulBuilds(deviceType).then(function (
|
||||
applications,
|
||||
) {
|
||||
applicationInfoSpinner.stop();
|
||||
if (applications.length === 0) {
|
||||
exitWithExpectedError(
|
||||
`You have no apps with successful releases for a '${deviceType}' device type.`,
|
||||
);
|
||||
}
|
||||
return getCliForm().ask({
|
||||
message: 'Select an application',
|
||||
type: 'list',
|
||||
choices: applications.map((app) => ({
|
||||
name: app.app_name,
|
||||
value: app,
|
||||
})),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const selectApplicationCommit = function (releases) {
|
||||
const { exitWithExpectedError } = require('../errors');
|
||||
|
||||
if (releases.length === 0) {
|
||||
exitWithExpectedError('This application has no successful releases.');
|
||||
}
|
||||
const DEFAULT_CHOICE = { name: 'current', value: 'current' };
|
||||
const choices = [DEFAULT_CHOICE].concat(
|
||||
releases.map((release) => ({
|
||||
name: `${release.end_timestamp} - ${release.commit}`,
|
||||
value: release.commit,
|
||||
})),
|
||||
);
|
||||
return getCliForm().ask({
|
||||
message: 'Select a release',
|
||||
type: 'list',
|
||||
default: 'current',
|
||||
choices,
|
||||
});
|
||||
};
|
||||
|
||||
const offerToDisableAutomaticUpdates = async function (
|
||||
application,
|
||||
commit,
|
||||
pinDevice,
|
||||
) {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if (
|
||||
isCurrent(commit) ||
|
||||
!application.should_track_latest_release ||
|
||||
pinDevice
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const message = `\
|
||||
|
||||
This application 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
|
||||
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:
|
||||
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?\
|
||||
`;
|
||||
return getCliForm()
|
||||
.ask({
|
||||
message,
|
||||
type: 'confirm',
|
||||
})
|
||||
.then(function (update) {
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
return balena.pine.patch({
|
||||
resource: 'application',
|
||||
id: application.id,
|
||||
body: {
|
||||
should_track_latest_release: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('balena-sdk').BalenaSDK} balenaSdk
|
||||
* @param {string | number} appId
|
||||
* @returns {Promise<import('balena-sdk').Application & { should_be_running__release: [import('balena-sdk').Release?] }>}
|
||||
*/
|
||||
async function getAppWithReleases(balenaSdk, appId) {
|
||||
// @ts-ignore
|
||||
return balenaSdk.models.application.get(appId, {
|
||||
$expand: applicationExpandOptions,
|
||||
});
|
||||
}
|
||||
|
||||
async function prepareAndPreload(preloader, balenaSdk, options) {
|
||||
const { ExpectedError } = require('../errors');
|
||||
|
||||
await preloader.prepare();
|
||||
|
||||
const application = options.appId
|
||||
? await getAppWithReleases(balenaSdk, options.appId)
|
||||
: await selectApplication(preloader.config.deviceType);
|
||||
|
||||
/** @type {string} commit hash or the strings 'latest' or 'current' */
|
||||
let commit;
|
||||
|
||||
const appCommit = application.should_be_running__release[0]?.commit;
|
||||
|
||||
// Use the commit given as --commit or show an interactive commit selection menu
|
||||
if (options.commit) {
|
||||
if (isCurrent(options.commit)) {
|
||||
if (!appCommit) {
|
||||
throw new Error(
|
||||
`Unexpected empty commit hash for app ID "${application.id}"`,
|
||||
);
|
||||
}
|
||||
// handle `--commit current` (and its `--commit latest` synonym)
|
||||
commit = 'latest';
|
||||
} else {
|
||||
const release = _.find(application.owns__release, (r) =>
|
||||
r.commit.startsWith(options.commit),
|
||||
);
|
||||
if (!release) {
|
||||
throw new ExpectedError(
|
||||
`There is no release matching commit "${options.commit}"`,
|
||||
);
|
||||
}
|
||||
commit = release.commit;
|
||||
}
|
||||
} else {
|
||||
// this could have the value 'current'
|
||||
commit = await selectApplicationCommit(application.owns__release);
|
||||
}
|
||||
|
||||
await preloader.setAppIdAndCommit(
|
||||
application.id,
|
||||
isCurrent(commit) ? appCommit : commit,
|
||||
);
|
||||
|
||||
// Propose to disable automatic app updates if the commit is not the current release
|
||||
await offerToDisableAutomaticUpdates(application, commit, options.pinDevice);
|
||||
|
||||
// All options are ready: preload the image.
|
||||
await preloader.preload();
|
||||
}
|
||||
|
||||
const preloadOptions = dockerUtils.appendConnectionOptions([
|
||||
{
|
||||
signature: 'app',
|
||||
parameter: 'appId',
|
||||
description: 'Name, slug or numeric ID of the application to preload',
|
||||
alias: 'a',
|
||||
},
|
||||
{
|
||||
signature: 'commit',
|
||||
parameter: 'hash',
|
||||
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 .\
|
||||
`,
|
||||
alias: 'c',
|
||||
},
|
||||
{
|
||||
signature: 'splash-image',
|
||||
parameter: 'splashImage.png',
|
||||
description: 'path to a png image to replace the splash screen',
|
||||
alias: 's',
|
||||
},
|
||||
{
|
||||
signature: 'dont-check-arch',
|
||||
boolean: true,
|
||||
description:
|
||||
'Disables check for matching architecture in image and application',
|
||||
},
|
||||
{
|
||||
signature: 'pin-device-to-release',
|
||||
boolean: true,
|
||||
description:
|
||||
'Pin the preloaded device to the preloaded release on provision',
|
||||
alias: 'p',
|
||||
},
|
||||
{
|
||||
signature: 'add-certificate',
|
||||
parameter: 'certificate.crt',
|
||||
description: `\
|
||||
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
|
||||
The file name must end with '.crt' and must not be already contained in the preloader's
|
||||
/etc/ssl/certs folder.
|
||||
Can be repeated to add multiple certificates.\
|
||||
`,
|
||||
},
|
||||
]);
|
||||
// Remove dockerPort `-p` alias as it conflicts with pin-device-to-release
|
||||
delete _.find(preloadOptions, { signature: 'dockerPort' }).alias;
|
||||
|
||||
export const preload = {
|
||||
signature: 'preload <image>',
|
||||
description: 'preload an app on a disk image (or Edison zip archive)',
|
||||
help: `\
|
||||
Preload a balena application release (app images/containers), 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.
|
||||
|
||||
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
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png
|
||||
$ balena preload balena.img\
|
||||
`,
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
options: preloadOptions,
|
||||
async action(params, options) {
|
||||
const balena = getBalenaSdk();
|
||||
const balenaPreload = require('balena-preload');
|
||||
const visuals = getVisuals();
|
||||
const nodeCleanup = require('node-cleanup');
|
||||
const { ExpectedError, instanceOf } = require('../errors');
|
||||
|
||||
const progressBars = {};
|
||||
|
||||
const progressHandler = function (event) {
|
||||
let progressBar = progressBars[event.name];
|
||||
if (!progressBar) {
|
||||
progressBar = progressBars[event.name] = new visuals.Progress(
|
||||
event.name,
|
||||
);
|
||||
}
|
||||
return progressBar.update({ percentage: event.percentage });
|
||||
};
|
||||
|
||||
const spinners = {};
|
||||
|
||||
const spinnerHandler = function (event) {
|
||||
let spinner = spinners[event.name];
|
||||
if (!spinner) {
|
||||
spinner = spinners[event.name] = new visuals.Spinner(event.name);
|
||||
}
|
||||
if (event.action === 'start') {
|
||||
return spinner.start();
|
||||
} else {
|
||||
console.log();
|
||||
return spinner.stop();
|
||||
}
|
||||
};
|
||||
|
||||
options.commit = isCurrent(options.commit) ? 'latest' : options.commit;
|
||||
options.image = params.image;
|
||||
options.appId = options.app;
|
||||
delete options.app;
|
||||
|
||||
options.splashImage = options['splash-image'];
|
||||
delete options['splash-image'];
|
||||
|
||||
options.dontCheckArch = options['dont-check-arch'] || false;
|
||||
delete options['dont-check-arch'];
|
||||
if (options.dontCheckArch && !options.appId) {
|
||||
throw new ExpectedError(
|
||||
'You need to specify an app id if you disable the architecture check.',
|
||||
);
|
||||
}
|
||||
|
||||
options.pinDevice = options['pin-device-to-release'] || false;
|
||||
delete options['pin-device-to-release'];
|
||||
|
||||
let certificates;
|
||||
if (Array.isArray(options['add-certificate'])) {
|
||||
certificates = options['add-certificate'];
|
||||
} else if (options['add-certificate'] === undefined) {
|
||||
certificates = [];
|
||||
} else {
|
||||
certificates = [options['add-certificate']];
|
||||
}
|
||||
for (let certificate of certificates) {
|
||||
if (!certificate.endsWith('.crt')) {
|
||||
throw new ExpectedError('Certificate file name must end with ".crt"');
|
||||
}
|
||||
}
|
||||
|
||||
// Get a configured dockerode instance
|
||||
const docker = await dockerUtils.getDocker(options);
|
||||
const preloader = new balenaPreload.Preloader(
|
||||
null,
|
||||
docker,
|
||||
options.appId,
|
||||
options.commit,
|
||||
options.image,
|
||||
options.splashImage,
|
||||
options.proxy,
|
||||
options.dontCheckArch,
|
||||
options.pinDevice,
|
||||
certificates,
|
||||
);
|
||||
|
||||
let gotSignal = false;
|
||||
|
||||
nodeCleanup(function (_exitCode, signal) {
|
||||
if (signal) {
|
||||
gotSignal = true;
|
||||
nodeCleanup.uninstall(); // don't call cleanup handler again
|
||||
preloader.cleanup().then(() => {
|
||||
// calling process.exit() won't inform parent process of signal
|
||||
process.kill(process.pid, signal);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
preloader.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
preloader.on('progress', progressHandler);
|
||||
preloader.on('spinner', spinnerHandler);
|
||||
|
||||
try {
|
||||
await new Promise(function (resolve, reject) {
|
||||
preloader.on('error', reject);
|
||||
resolve(prepareAndPreload(preloader, balena, options));
|
||||
});
|
||||
} catch (err) {
|
||||
if (instanceOf(err, balena.errors.BalenaError)) {
|
||||
const code = err.code ? `(${err.code})` : '';
|
||||
throw new ExpectedError(`${err.message} ${code}`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (!gotSignal) {
|
||||
await preloader.cleanup();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as capitano from 'capitano';
|
||||
import * as actions from './actions';
|
||||
import * as events from './events';
|
||||
import { promisify } from 'util';
|
||||
|
||||
capitano.permission('user', (done) =>
|
||||
require('./utils/patterns').checkLoggedIn().then(done, done),
|
||||
);
|
||||
|
||||
capitano.command({
|
||||
signature: '*',
|
||||
action(_params, _options, done) {
|
||||
capitano.execute({ command: 'help' }, done);
|
||||
process.exitCode = process.exitCode || 1;
|
||||
},
|
||||
});
|
||||
|
||||
capitano.globalOption({
|
||||
signature: 'help',
|
||||
boolean: true,
|
||||
alias: 'h',
|
||||
});
|
||||
|
||||
capitano.globalOption({
|
||||
signature: 'version',
|
||||
boolean: true,
|
||||
alias: 'v',
|
||||
});
|
||||
|
||||
// ---------- Help Module ----------
|
||||
capitano.command(actions.help.help);
|
||||
|
||||
// ---------- Preload Module ----------
|
||||
capitano.command(actions.preload);
|
||||
|
||||
// ------------ Local build and deploy -------
|
||||
capitano.command(actions.deploy);
|
||||
|
||||
export function run(argv: string[]) {
|
||||
const cli = capitano.parse(argv.slice(2));
|
||||
const runCommand = function () {
|
||||
const capitanoExecuteAsync = promisify(capitano.execute);
|
||||
if (cli.global?.help) {
|
||||
return capitanoExecuteAsync({
|
||||
command: `help ${cli.command ?? ''}`,
|
||||
});
|
||||
} else {
|
||||
return capitanoExecuteAsync(cli);
|
||||
}
|
||||
};
|
||||
|
||||
const trackCommand = function () {
|
||||
const getMatchCommandAsync = promisify(capitano.state.getMatchCommand);
|
||||
return getMatchCommandAsync(cli.command).then(function (command) {
|
||||
// cmdSignature is literally a string like, for example:
|
||||
// "push <applicationOrDevice>"
|
||||
// ("applicationOrDevice" is NOT replaced with its actual value)
|
||||
// In case of failures like an nonexistent or invalid command,
|
||||
// command.signature.toString() returns '*'
|
||||
const cmdSignature = command.signature.toString();
|
||||
return events.trackCommand(cmdSignature);
|
||||
});
|
||||
};
|
||||
|
||||
return Promise.all([trackCommand(), runCommand()]).catch(
|
||||
require('./errors').handleError,
|
||||
);
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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 { Main } from '@oclif/command';
|
||||
|
||||
import { trackPromise } from './hooks/prerun/track';
|
||||
|
||||
class CustomMain extends Main {
|
||||
protected _helpOverride(): boolean {
|
||||
// Disable oclif's default handler for the 'version' command
|
||||
if (['-v', '--version', 'version'].includes(this.argv[0])) {
|
||||
return false;
|
||||
} else {
|
||||
return super._helpOverride();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import type { AppOptions } from './preparser';
|
||||
|
||||
/**
|
||||
* oclif CLI entrypoint
|
||||
*/
|
||||
export async function run(command: string[], options: AppOptions) {
|
||||
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)
|
||||
// (Avoid `error instanceof ExitError` here for the reasons explained
|
||||
// in the CONTRIBUTING.md file regarding the `instanceof` operator.)
|
||||
if (error.oclif?.exit === 0) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
try {
|
||||
await Promise.all([trackPromise, runPromise]);
|
||||
} catch (err) {
|
||||
await (await import('./errors')).handleError(err);
|
||||
}
|
||||
}
|
185
lib/app.ts
185
lib/app.ts
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,73 +15,138 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { CliSettings } from './utils/bootstrap';
|
||||
import { onceAsync, stripIndent } from './utils/lazy';
|
||||
|
||||
/**
|
||||
* CLI entrypoint, but see also `bin/balena` and `bin/balena-dev` which
|
||||
* call this function.
|
||||
* Sentry.io setup
|
||||
* @see https://docs.sentry.io/error-reporting/quickstart/?platform=node
|
||||
*/
|
||||
export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
dsn: config.sentryDsn,
|
||||
release: packageJSON.version,
|
||||
});
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setExtras({
|
||||
is_pkg: !!(process as any).pkg,
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
});
|
||||
});
|
||||
return Sentry.getCurrentHub();
|
||||
});
|
||||
|
||||
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/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Setup balena-sdk options that are shared with imported packages */
|
||||
function setupBalenaSdkSharedOptions(settings: CliSettings) {
|
||||
const BalenaSdk = require('balena-sdk') as typeof import('balena-sdk');
|
||||
BalenaSdk.setSharedOptions({
|
||||
apiUrl: settings.get<string>('apiUrl'),
|
||||
dataDirectory: settings.get<string>('dataDirectory'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Addresses the console warning:
|
||||
* (node:49500) MaxListenersExceededWarning: Possible EventEmitter memory
|
||||
* leak detected. 11 error listeners added. Use emitter.setMaxListeners() to
|
||||
* increase limit
|
||||
*/
|
||||
export function setMaxListeners(maxListeners: number) {
|
||||
require('events').EventEmitter.defaultMaxListeners = maxListeners;
|
||||
}
|
||||
|
||||
/** Selected CLI initialization steps */
|
||||
async function init() {
|
||||
if (process.env.BALENARC_NO_SENTRY) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
} else {
|
||||
await setupSentry();
|
||||
}
|
||||
checkNodeVersion();
|
||||
|
||||
const settings = new CliSettings();
|
||||
|
||||
// Proxy setup should be done early on, before loading balena-sdk
|
||||
await (await import('./utils/proxy')).setupGlobalHttpProxy(settings);
|
||||
|
||||
setupBalenaSdkSharedOptions(settings);
|
||||
|
||||
// check for CLI updates once a day
|
||||
(await import('./utils/update')).notify();
|
||||
}
|
||||
|
||||
/** 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)
|
||||
// (Avoid `error instanceof ExitError` here for the reasons explained
|
||||
// in the CONTRIBUTING.md file regarding the `instanceof` operator.)
|
||||
if (error.oclif?.exit === 0) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
const { trackPromise } = await import('./hooks/prerun/track');
|
||||
await Promise.all([trackPromise, runPromise]);
|
||||
}
|
||||
|
||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||
export async function run(
|
||||
cliArgs = process.argv,
|
||||
options: import('./preparser').AppOptions = {},
|
||||
) {
|
||||
// DEBUG set to falsy for negative values else is truthy
|
||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
process.env.DEBUG?.toLowerCase(),
|
||||
)
|
||||
? ''
|
||||
: '1';
|
||||
|
||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
||||
// for use of the standalone zip package. See pkgExec function.
|
||||
if (cliArgs.length > 3 && cliArgs[2] === 'pkgExec') {
|
||||
return pkgExec(cliArgs[3], cliArgs.slice(4));
|
||||
}
|
||||
|
||||
const { globalInit } = await import('./app-common');
|
||||
const { routeCliFramework } = await import('./preparser');
|
||||
|
||||
// globalInit() must be called very early on (before other imports) because
|
||||
// it sets up Sentry error reporting, global HTTP proxy settings, balena-sdk
|
||||
// shared options, and performs node version requirement checks.
|
||||
await globalInit();
|
||||
await routeCliFramework(cliArgs, options);
|
||||
|
||||
// Windows fix: reading from stdin prevents the process from exiting
|
||||
process.stdin.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the 'pkgExec' command, used as a way to provide a Node.js
|
||||
* interpreter for child_process.spawn()-like operations when the CLI is
|
||||
* executing as a standalone zip package (built-in Node interpreter) and
|
||||
* the system may not have a separate Node.js installation. A present use
|
||||
* case is a patched version of the 'windosu' package that requires a
|
||||
* Node.js interpreter to spawn a privileged child process.
|
||||
*
|
||||
* @param modFunc Path to a JS module that will be executed via require().
|
||||
* The modFunc argument may optionally contain a function name separated
|
||||
* by '::', for example '::main' in:
|
||||
* 'C:\\snapshot\\balena-cli\\node_modules\\windosu\\lib\\pipe.js::main'
|
||||
* in which case that function is executed in the require'd module.
|
||||
* @param args Optional arguments to passed through process.argv and as
|
||||
* arguments to the function specified via modFunc.
|
||||
*/
|
||||
async function pkgExec(modFunc: string, args: string[]) {
|
||||
const [modPath, funcName] = modFunc.split('::');
|
||||
let replacedModPath = modPath;
|
||||
const match = modPath
|
||||
.replace(/\\/g, '/')
|
||||
.match(/\/snapshot\/balena-cli\/(.+)/);
|
||||
if (match) {
|
||||
replacedModPath = `../${match[1]}`;
|
||||
}
|
||||
process.argv = [process.argv[0], process.argv[1], ...args];
|
||||
try {
|
||||
const mod: any = await import(replacedModPath);
|
||||
if (funcName) {
|
||||
await mod[funcName](...args);
|
||||
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
|
||||
normalizeEnvVars();
|
||||
|
||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
||||
// for use of the standalone zip package. See pkgExec function.
|
||||
if (cliArgs.length > 3 && cliArgs[2] === 'pkgExec') {
|
||||
return pkgExec(cliArgs[3], cliArgs.slice(4));
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
const args = await preparseArgs(cliArgs);
|
||||
await oclifRun(args, options);
|
||||
} catch (err) {
|
||||
console.error(`Error executing pkgExec "${modFunc}" [${args.join()}]`);
|
||||
console.error(err);
|
||||
await (await import('./errors')).handleError(err);
|
||||
} finally {
|
||||
// Windows fix: reading from stdin prevents the process from exiting
|
||||
process.stdin.pause();
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Balena CLI - Error</title>
|
||||
<title>balena CLI - Error</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="./static/style.css" inline>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Balena CLI - Success</title>
|
||||
<title>balena CLI - Success</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="./static/style.css" inline>
|
||||
|
@ -63,30 +63,27 @@ export const getDashboardLoginURL = (callbackUrl: string) => {
|
||||
* if loggedIn
|
||||
* console.log('Token is valid!')
|
||||
*/
|
||||
export const loginIfTokenValid = async (token: string) => {
|
||||
export const loginIfTokenValid = async (token: string): Promise<boolean> => {
|
||||
if (_.isEmpty(token?.trim())) {
|
||||
return false;
|
||||
}
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return balena.auth
|
||||
.getToken()
|
||||
.catchReturn(undefined)
|
||||
.then((currentToken) =>
|
||||
balena.auth
|
||||
.loginWithToken(token)
|
||||
.return(token)
|
||||
.then(balena.auth.isLoggedIn)
|
||||
.tap((isLoggedIn) => {
|
||||
if (isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
let currentToken;
|
||||
try {
|
||||
currentToken = await balena.auth.getToken();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (currentToken != null) {
|
||||
return balena.auth.loginWithToken(currentToken);
|
||||
} else {
|
||||
return balena.auth.logout();
|
||||
}
|
||||
}),
|
||||
);
|
||||
await balena.auth.loginWithToken(token);
|
||||
const isLoggedIn = await balena.auth.isLoggedIn();
|
||||
if (!isLoggedIn) {
|
||||
if (currentToken != null) {
|
||||
await balena.auth.loginWithToken(currentToken);
|
||||
} else {
|
||||
await balena.auth.logout();
|
||||
}
|
||||
}
|
||||
return isLoggedIn;
|
||||
};
|
||||
|
@ -19,8 +19,7 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { Release } from 'balena-sdk';
|
||||
import type { Release } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
@ -58,15 +57,14 @@ export default class AppCmd extends Command {
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppCmd);
|
||||
|
||||
const application = (await getBalenaSdk().models.application.get(
|
||||
tryAsInteger(params.name),
|
||||
{
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
},
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const application = (await getApplication(getBalenaSdk(), params.name, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
},
|
||||
)) as ApplicationWithDeviceType & {
|
||||
})) as ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
};
|
||||
|
82
lib/commands/app/purge.ts
Normal file
82
lib/commands/app/purge.ts
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @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 {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Purge data from an application.
|
||||
|
||||
Purge data from all devices belonging to an application.
|
||||
This will clear the application's /data directory.
|
||||
`;
|
||||
public static examples = ['$ balena app purge MyApp'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app purge <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// balena.models.application.purge only accepts a numeric id
|
||||
// so we must first fetch the app to get it's id, if we have been given a name
|
||||
let nameOrId = tryAsInteger(params.name);
|
||||
|
||||
if (typeof nameOrId === 'string') {
|
||||
const app = await balena.models.application.get(nameOrId);
|
||||
nameOrId = app.id;
|
||||
}
|
||||
|
||||
try {
|
||||
await balena.models.application.purge(nameOrId);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('no online device(s) found')) {
|
||||
// application.purge throws an error if no devices are online
|
||||
// ignore in this case.
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
136
lib/commands/app/rename.ts
Normal file
136
lib/commands/app/rename.ts
Normal file
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @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 type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
newName?: string;
|
||||
}
|
||||
|
||||
export default class AppRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Rename an application.
|
||||
|
||||
Rename an application.
|
||||
|
||||
Note, if the \`newName\` parameter is omitted, it will be
|
||||
prompted for interactively.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena app rename OldName',
|
||||
'$ balena app rename OldName NewName',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'newName',
|
||||
description: 'the new name for the application',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app rename <name> [newName]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
|
||||
|
||||
const { ExpectedError, instanceOf } = await import('../../errors');
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Get app
|
||||
let app;
|
||||
try {
|
||||
app = await getApplication(balena, params.name, {
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: ['is_legacy'],
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||
if (instanceOf(e, BalenaApplicationNotFound)) {
|
||||
throw new ExpectedError(`Application ${params.name} not found.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Check app supports renaming
|
||||
const appType = (app.application_type as ApplicationType[])?.[0];
|
||||
if (appType.is_legacy) {
|
||||
throw new ExpectedError(
|
||||
`Application ${params.name} is of 'legacy' type, and cannot be renamed.`,
|
||||
);
|
||||
}
|
||||
|
||||
const { validateApplicationName } = await import('../../utils/validation');
|
||||
const newName =
|
||||
params.newName ||
|
||||
(await getCliForm().ask({
|
||||
message: 'Please enter the new name for this application:',
|
||||
type: 'input',
|
||||
validate: validateApplicationName,
|
||||
})) ||
|
||||
'';
|
||||
|
||||
try {
|
||||
await this.renameApplication(balena, app.id, newName);
|
||||
} catch (e) {
|
||||
// BalenaRequestError: Request error: Unique key constraint violated
|
||||
if ((e.message || '').toLowerCase().includes('unique')) {
|
||||
throw new ExpectedError(
|
||||
`Error: application ${params.name} already exists.`,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log(`Application ${params.name} renamed to ${newName}`);
|
||||
}
|
||||
|
||||
async renameApplication(balena: BalenaSDK, id: number, newName: string) {
|
||||
return balena.pine.patch<Application>({
|
||||
resource: 'application',
|
||||
id,
|
||||
body: {
|
||||
app_name: newName,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ export default class AppRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart an application.
|
||||
|
||||
Restart all devices that belongs to a certain application.
|
||||
Restart all devices belonging to an application.
|
||||
`;
|
||||
public static examples = ['$ balena app restart MyApp'];
|
||||
|
@ -17,14 +17,13 @@
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import * as compose from '../utils/compose';
|
||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
||||
import { composeCliFlags } from '../utils/compose_ts';
|
||||
import type { DockerCliFlags } from '../utils/docker';
|
||||
import { buildProject, composeCliFlags } from '../utils/compose_ts';
|
||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
|
||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
@ -94,7 +93,9 @@ ${dockerignoreHelp}
|
||||
}),
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
help: cf.help,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
// Revisit this in future release.
|
||||
help: flags.help({}),
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
@ -115,7 +116,10 @@ ${dockerignoreHelp}
|
||||
const logger = await Command.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
this.translateParams(params, options);
|
||||
// `build` accepts `source` as a parameter, but compose expects it as an option
|
||||
options.source = params.source;
|
||||
delete params.source;
|
||||
|
||||
await this.validateOptions(options, sdk);
|
||||
|
||||
const app = await this.getAppAndResolveArch(options);
|
||||
@ -139,21 +143,6 @@ ${dockerignoreHelp}
|
||||
logger.logSuccess('Build succeeded!');
|
||||
}
|
||||
|
||||
protected translateParams(params: ArgsDef, options: FlagsDef) {
|
||||
// Copy flags to those expected by other modules
|
||||
options.arg = options.buildArg;
|
||||
delete options.buildArg;
|
||||
options['image-list'] = options['cache-from'];
|
||||
delete options['cache-from'];
|
||||
|
||||
// `build` accepts `[source]` as a parameter, but compose expects it
|
||||
// as an option. swap them here
|
||||
if (options.source == null) {
|
||||
options.source = params.source;
|
||||
}
|
||||
delete params.source;
|
||||
}
|
||||
|
||||
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
||||
// Validate option combinations
|
||||
if (
|
||||
@ -230,7 +219,7 @@ ${dockerignoreHelp}
|
||||
arch: string;
|
||||
deviceType: string;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: any;
|
||||
buildOpts: BuildOpts;
|
||||
},
|
||||
) {
|
||||
const { loadProject } = await import('../utils/compose_ts');
|
||||
@ -249,21 +238,21 @@ ${dockerignoreHelp}
|
||||
);
|
||||
}
|
||||
|
||||
await compose.buildProject(
|
||||
await buildProject({
|
||||
docker,
|
||||
logger,
|
||||
project.path,
|
||||
project.name,
|
||||
project.composition,
|
||||
opts.arch,
|
||||
opts.deviceType,
|
||||
opts.buildEmulated,
|
||||
opts.buildOpts,
|
||||
composeOpts.inlineLogs,
|
||||
composeOpts.convertEol,
|
||||
composeOpts.dockerfilePath,
|
||||
composeOpts.nogitignore,
|
||||
composeOpts.multiDockerignore,
|
||||
);
|
||||
projectPath: project.path,
|
||||
projectName: project.name,
|
||||
composition: project.composition,
|
||||
arch: opts.arch,
|
||||
deviceType: opts.deviceType,
|
||||
emulated: opts.buildEmulated,
|
||||
buildOpts: opts.buildOpts,
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||
import type { Application, PineDeferred } from 'balena-sdk';
|
||||
import type { PineDeferred } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
version: string; // OS version
|
||||
@ -126,38 +126,52 @@ export default class ConfigGenerateCmd extends Command {
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
await this.validateOptions(options);
|
||||
|
||||
let deviceType = options.deviceType;
|
||||
// Get device | application
|
||||
let resource;
|
||||
let resourceDeviceType: string;
|
||||
let application: ApplicationWithDeviceType | null = null;
|
||||
let device:
|
||||
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
||||
| null = null;
|
||||
if (options.device != null) {
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
resource = (await balena.models.device.get(tryAsInteger(options.device), {
|
||||
$expand: {
|
||||
is_of__device_type: { $select: 'slug' },
|
||||
},
|
||||
})) as DeviceWithDeviceType & { belongs_to__application: PineDeferred };
|
||||
deviceType = deviceType || resource.is_of__device_type[0].slug;
|
||||
const rawDevice = await balena.models.device.get(
|
||||
tryAsInteger(options.device),
|
||||
{ $expand: { is_of__device_type: { $select: 'slug' } } },
|
||||
);
|
||||
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 = rawDevice as DeviceWithDeviceType & {
|
||||
belongs_to__application: PineDeferred;
|
||||
};
|
||||
resourceDeviceType = device.is_of__device_type[0].slug;
|
||||
} else {
|
||||
resource = (await balena.models.application.get(options.application!, {
|
||||
application = (await getApplication(balena, options.application!, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
})) as ApplicationWithDeviceType;
|
||||
deviceType = deviceType || resource.is_for__device_type[0].slug;
|
||||
resourceDeviceType = application.is_for__device_type[0].slug;
|
||||
}
|
||||
|
||||
const deviceType = options.deviceType || resourceDeviceType;
|
||||
|
||||
const deviceManifest = await balena.models.device.getManifestBySlug(
|
||||
deviceType!,
|
||||
deviceType,
|
||||
);
|
||||
|
||||
// Check compatibility if application and deviceType provided
|
||||
if (options.application && options.deviceType) {
|
||||
const appDeviceManifest = await balena.models.device.getManifestBySlug(
|
||||
deviceType!,
|
||||
resourceDeviceType,
|
||||
);
|
||||
|
||||
const helpers = await import('../../utils/helpers');
|
||||
@ -184,18 +198,15 @@ export default class ConfigGenerateCmd extends Command {
|
||||
);
|
||||
|
||||
let config;
|
||||
if ('uuid' in resource && resource.uuid != null) {
|
||||
if (device) {
|
||||
config = await generateDeviceConfig(
|
||||
resource,
|
||||
device,
|
||||
options.deviceApiKey || options['generate-device-api-key'] || undefined,
|
||||
answers,
|
||||
);
|
||||
} else {
|
||||
} else if (application) {
|
||||
answers.deviceType = deviceType;
|
||||
config = await generateApplicationConfig(
|
||||
resource as Application,
|
||||
answers,
|
||||
);
|
||||
config = await generateApplicationConfig(application, answers);
|
||||
}
|
||||
|
||||
// Output
|
352
lib/commands/deploy.ts
Normal file
352
lib/commands/deploy.ts
Normal file
@ -0,0 +1,352 @@
|
||||
/**
|
||||
* @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 type { ImageDescriptor } from 'resin-compose-parse';
|
||||
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import * as compose from '../utils/compose';
|
||||
import type {
|
||||
BuiltImage,
|
||||
ComposeCliFlags,
|
||||
ComposeOpts,
|
||||
} from '../utils/compose-types';
|
||||
import type { DockerCliFlags } from '../utils/docker';
|
||||
import {
|
||||
buildProject,
|
||||
composeCliFlags,
|
||||
isBuildConfig,
|
||||
} from '../utils/compose_ts';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
import type { Application, ApplicationType, DeviceType } from 'balena-sdk';
|
||||
|
||||
interface ApplicationWithArch extends Application {
|
||||
arch: string;
|
||||
}
|
||||
|
||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
source?: string;
|
||||
build: boolean;
|
||||
nologupload: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
appName: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export default class DeployCmd extends Command {
|
||||
public static description = `\
|
||||
Deploy a single image or a multicontainer project to a balena application.
|
||||
|
||||
Usage: \`deploy <appName> ([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
|
||||
(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.)
|
||||
|
||||
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.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
\`balena deploy <appOwnerUsername>/<appName>\`.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
${dockerignoreHelp}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena deploy myApp',
|
||||
'$ balena deploy myApp --build --source myBuildDir/',
|
||||
'$ balena deploy myApp myApp/myImage',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'appName',
|
||||
description: 'the name of the application to deploy to',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
description: 'the image to deploy',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'deploy <appName> [image]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
source: flags.string({
|
||||
description:
|
||||
'specify an alternate source directory; default is the working directory',
|
||||
char: 's',
|
||||
}),
|
||||
build: flags.boolean({
|
||||
description: 'force a rebuild before deploy',
|
||||
char: 'b',
|
||||
}),
|
||||
nologupload: flags.boolean({
|
||||
description:
|
||||
"don't upload build logs to the dashboard with image (if building)",
|
||||
}),
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
// Revisit this in future release.
|
||||
help: flags.help({}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
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;
|
||||
|
||||
if (image != null && options.build) {
|
||||
throw new ExpectedError(
|
||||
'Build option is not applicable when specifying an image',
|
||||
);
|
||||
}
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
||||
'../utils/compose_ts'
|
||||
);
|
||||
|
||||
if (image) {
|
||||
options['registry-secrets'] = await getRegistrySecrets(
|
||||
sdk,
|
||||
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'],
|
||||
});
|
||||
options.dockerfile = dockerfilePath;
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
}
|
||||
|
||||
const helpers = await import('../utils/helpers');
|
||||
const app = await helpers.getAppWithArch(appName);
|
||||
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||
dockerUtils.getDocker(options),
|
||||
dockerUtils.generateBuildOpts(options),
|
||||
compose.generateOpts(options),
|
||||
]);
|
||||
|
||||
await this.deployProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
appName, // may be prefixed by 'owner/', unlike app.app_name
|
||||
image,
|
||||
shouldPerformBuild: !!options.build,
|
||||
shouldUploadLogs: !options.nologupload,
|
||||
buildEmulated: !!options.emulated,
|
||||
buildOpts,
|
||||
});
|
||||
}
|
||||
|
||||
async deployProject(
|
||||
docker: import('docker-toolbelt'),
|
||||
logger: import('../utils/logger'),
|
||||
composeOpts: ComposeOpts,
|
||||
opts: {
|
||||
app: ApplicationWithArch; // the application instance to deploy to
|
||||
appName: string;
|
||||
image?: string;
|
||||
dockerfilePath?: string; // alternative Dockerfile
|
||||
shouldPerformBuild: boolean;
|
||||
shouldUploadLogs: boolean;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: any; // arguments to forward to docker build command
|
||||
},
|
||||
) {
|
||||
const _ = await import('lodash');
|
||||
const doodles = await import('resin-doodles');
|
||||
const sdk = getBalenaSdk();
|
||||
const { deployProject: $deployProject, loadProject } = await import(
|
||||
'../utils/compose_ts'
|
||||
);
|
||||
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
|
||||
try {
|
||||
const project = await loadProject(logger, composeOpts, opts.image);
|
||||
if (project.descriptors.length > 1 && !appType?.supports_multicontainer) {
|
||||
throw new ExpectedError(
|
||||
'Target application does not support multiple containers. Aborting!',
|
||||
);
|
||||
}
|
||||
|
||||
// find which services use images that already exist locally
|
||||
let servicesToSkip: string[] = await Promise.all(
|
||||
project.descriptors.map(async function (d: ImageDescriptor) {
|
||||
// unconditionally build (or pull) if explicitly requested
|
||||
if (opts.shouldPerformBuild) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
await docker
|
||||
.getImage((isBuildConfig(d.image) ? d.image.tag : d.image) || '')
|
||||
.inspect();
|
||||
|
||||
return d.serviceName;
|
||||
} catch {
|
||||
// Ignore
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
servicesToSkip = servicesToSkip.filter((d) => !!d);
|
||||
|
||||
// multibuild takes in a composition and always attempts to
|
||||
// build or pull all services. we workaround that here by
|
||||
// passing a modified composition.
|
||||
const compositionToBuild = _.cloneDeep(project.composition);
|
||||
compositionToBuild.services = _.omit(
|
||||
compositionToBuild.services,
|
||||
servicesToSkip,
|
||||
);
|
||||
let builtImagesByService: Dictionary<BuiltImage> = {};
|
||||
if (_.size(compositionToBuild.services) === 0) {
|
||||
logger.logInfo(
|
||||
'Everything is up to date (use --build to force a rebuild)',
|
||||
);
|
||||
} else {
|
||||
const builtImages = await buildProject({
|
||||
docker,
|
||||
logger,
|
||||
projectPath: project.path,
|
||||
projectName: project.name,
|
||||
composition: compositionToBuild,
|
||||
arch: opts.app.arch,
|
||||
deviceType: (opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
|
||||
emulated: opts.buildEmulated,
|
||||
buildOpts: opts.buildOpts,
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
||||
}
|
||||
const images: BuiltImage[] = project.descriptors.map(
|
||||
(d) =>
|
||||
builtImagesByService[d.serviceName] ?? {
|
||||
serviceName: d.serviceName,
|
||||
name: (isBuildConfig(d.image) ? d.image.tag : d.image) || '',
|
||||
logs: 'Build skipped; image for service already exists.',
|
||||
props: {},
|
||||
},
|
||||
);
|
||||
|
||||
let release;
|
||||
if (appType?.is_legacy) {
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
|
||||
const msg = getChalk().yellow(
|
||||
'Target application requires legacy deploy method.',
|
||||
);
|
||||
logger.logWarn(msg);
|
||||
|
||||
const [token, username, url, options] = await Promise.all([
|
||||
sdk.auth.getToken(),
|
||||
sdk.auth.whoami(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
{
|
||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
appName: opts.appName,
|
||||
imageName: images[0].name,
|
||||
buildLogs: images[0].logs,
|
||||
shouldUploadLogs: opts.shouldUploadLogs,
|
||||
},
|
||||
]);
|
||||
const releaseId = await deployLegacy(
|
||||
docker,
|
||||
logger,
|
||||
token,
|
||||
username,
|
||||
url,
|
||||
options,
|
||||
);
|
||||
|
||||
release = await sdk.models.release.get(releaseId, {
|
||||
$select: ['commit'],
|
||||
});
|
||||
} else {
|
||||
const [userId, auth, apiEndpoint] = await Promise.all([
|
||||
sdk.auth.getUserId(),
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('apiUrl'),
|
||||
]);
|
||||
release = await $deployProject(
|
||||
docker,
|
||||
logger,
|
||||
project.composition,
|
||||
images,
|
||||
opts.app.id,
|
||||
userId,
|
||||
`Bearer ${auth}`,
|
||||
apiEndpoint,
|
||||
!opts.shouldUploadLogs,
|
||||
);
|
||||
}
|
||||
|
||||
logger.outputDeferredMessages();
|
||||
logger.logSuccess('Deploy succeeded!');
|
||||
logger.logSuccess(`Release: ${release.commit}`);
|
||||
console.log();
|
||||
console.log(doodles.getDoodle()); // Show charlie
|
||||
console.log();
|
||||
} catch (err) {
|
||||
logger.logError('Deploy failed');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,15 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
device_type?: string;
|
||||
commit?: string;
|
||||
last_seen?: string;
|
||||
memory_usage_mb: number | null;
|
||||
memory_total_mb: number | null;
|
||||
memory_usage_percent?: number;
|
||||
storage_usage_mb: number | null;
|
||||
storage_total_mb: number | null;
|
||||
storage_usage_percent?: number;
|
||||
cpu_temp_c: number | null;
|
||||
cpu_usage_percent: number | null;
|
||||
undervoltage_detected?: boolean;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
@ -85,6 +94,15 @@ export default class DeviceCmd extends Command {
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'memory_usage',
|
||||
'memory_total',
|
||||
'storage_block_device',
|
||||
'storage_usage',
|
||||
'storage_total',
|
||||
'cpu_usage',
|
||||
'cpu_temp',
|
||||
'cpu_id',
|
||||
'is_undervolted',
|
||||
],
|
||||
...expandForAppName,
|
||||
})) as ExtendedDevice;
|
||||
@ -98,9 +116,50 @@ export default class DeviceCmd extends Command {
|
||||
: 'N/a';
|
||||
|
||||
device.device_type = device.is_of__device_type[0].slug;
|
||||
device.commit = (device.is_running__release as Release[])[0].commit;
|
||||
|
||||
const isRunningRelease = device.is_running__release as Release[];
|
||||
device.commit = isRunningRelease?.[0] ? isRunningRelease[0].commit : 'N/a';
|
||||
|
||||
device.last_seen = device.last_connectivity_event ?? undefined;
|
||||
|
||||
// Memory/Storage are really MiB
|
||||
// Consider changing headings to MiB once we can do lowercase
|
||||
|
||||
device.memory_usage_mb = device.memory_usage;
|
||||
device.memory_total_mb = device.memory_total;
|
||||
|
||||
device.storage_usage_mb = device.storage_usage;
|
||||
device.storage_total_mb = device.storage_total;
|
||||
|
||||
device.cpu_temp_c = device.cpu_temp;
|
||||
device.cpu_usage_percent = device.cpu_usage;
|
||||
|
||||
// Only show undervoltage status if true
|
||||
// API sends false even for devices which are not detecting this.
|
||||
if (device.is_undervolted) {
|
||||
device.undervoltage_detected = device.is_undervolted;
|
||||
}
|
||||
|
||||
if (
|
||||
device.memory_usage != null &&
|
||||
device.memory_total != null &&
|
||||
device.memory_total !== 0
|
||||
) {
|
||||
device.memory_usage_percent = Math.round(
|
||||
(device.memory_usage / device.memory_total) * 100,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
device.storage_usage != null &&
|
||||
device.storage_total != null &&
|
||||
device.storage_total !== 0
|
||||
) {
|
||||
device.storage_usage_percent = Math.round(
|
||||
(device.storage_usage / device.storage_total) * 100,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
@ -119,6 +178,17 @@ export default class DeviceCmd extends Command {
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
'cpu_usage_percent',
|
||||
'cpu_temp_c',
|
||||
'cpu_id',
|
||||
'memory_usage_mb',
|
||||
'memory_total_mb',
|
||||
'memory_usage_percent',
|
||||
'storage_block_device',
|
||||
'storage_usage_mb',
|
||||
'storage_total_mb',
|
||||
'storage_usage_percent',
|
||||
'undervoltage_detected',
|
||||
]),
|
||||
);
|
||||
}
|
@ -84,18 +84,19 @@ export default class DeviceInitCmd extends Command {
|
||||
const tmp = await import('tmp');
|
||||
const tmpNameAsync = promisify(tmp.tmpName);
|
||||
tmp.setGracefulCleanup();
|
||||
const balena = getBalenaSdk();
|
||||
const { downloadOSImage } = await import('../../utils/cloud');
|
||||
const Logger = await import('../../utils/logger');
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
const logger = await Command.getLogger();
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
// Get application and
|
||||
const application = (await balena.models.application.get(
|
||||
const application = (await getApplication(
|
||||
balena,
|
||||
options['application'] ||
|
||||
(await (await import('../../utils/patterns')).selectApplication()),
|
||||
{
|
@ -17,7 +17,7 @@
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import type { Application } from 'balena-sdk';
|
||||
import type { Application, BalenaSDK } from 'balena-sdk';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
@ -59,7 +59,6 @@ export default class DeviceMoveCmd extends Command {
|
||||
name: 'uuid',
|
||||
description:
|
||||
'comma-separated list (no blank spaces) of device UUIDs to be moved',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
@ -81,21 +80,25 @@ export default class DeviceMoveCmd extends Command {
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
// Parse ids string into array of correct types
|
||||
const deviceIds: Array<string | number> = params.uuid
|
||||
.split(',')
|
||||
.map((id) => tryAsInteger(id));
|
||||
|
||||
// Get devices
|
||||
const devices = await Promise.all(
|
||||
params.uuid
|
||||
.split(',')
|
||||
.map(
|
||||
(uuid) =>
|
||||
balena.models.device.get(uuid, expandForAppName) as Promise<
|
||||
ExtendedDevice
|
||||
>,
|
||||
),
|
||||
deviceIds.map(
|
||||
(uuid) =>
|
||||
balena.models.device.get(uuid, expandForAppName) as Promise<
|
||||
ExtendedDevice
|
||||
>,
|
||||
),
|
||||
);
|
||||
|
||||
// Map application name for each device
|
||||
for (const device of devices) {
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
@ -104,55 +107,12 @@ export default class DeviceMoveCmd extends Command {
|
||||
}
|
||||
|
||||
// Get destination application
|
||||
let application;
|
||||
if (options.application) {
|
||||
application = options.application;
|
||||
} else {
|
||||
const [deviceDeviceTypes, deviceTypes] = await Promise.all([
|
||||
Promise.all(
|
||||
devices.map((device) =>
|
||||
balena.models.device.getManifestBySlug(
|
||||
device.is_of__device_type[0].slug,
|
||||
),
|
||||
),
|
||||
),
|
||||
balena.models.config.getDeviceTypes(),
|
||||
]);
|
||||
const application =
|
||||
options.application ||
|
||||
(await this.interactivelySelectApplication(balena, devices));
|
||||
|
||||
const compatibleDeviceTypes = deviceTypes.filter((dt) =>
|
||||
deviceDeviceTypes.every(
|
||||
(deviceDeviceType) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceDeviceType.arch,
|
||||
dt.arch,
|
||||
) &&
|
||||
!!dt.isDependent === !!deviceDeviceType.isDependent &&
|
||||
dt.state !== 'DISCONTINUED',
|
||||
),
|
||||
);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
try {
|
||||
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
|
||||
devices.some((device) => device.application_name !== app.app_name),
|
||||
true,
|
||||
);
|
||||
} catch (err) {
|
||||
if (deviceDeviceTypes.length) {
|
||||
throw new ExpectedError(
|
||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
for (const uuid of params.uuid.split(',')) {
|
||||
// Move each device
|
||||
for (const uuid of deviceIds) {
|
||||
try {
|
||||
await balena.models.device.move(uuid, tryAsInteger(application));
|
||||
console.info(`${uuid} was moved to ${application}`);
|
||||
@ -162,4 +122,53 @@ export default class DeviceMoveCmd extends Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async interactivelySelectApplication(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
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 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
|
||||
devices.some((device) => device.application_name !== app.app_name),
|
||||
true,
|
||||
);
|
||||
return application;
|
||||
} catch (err) {
|
||||
if (deviceDeviceTypes.length) {
|
||||
throw new ExpectedError(
|
||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
80
lib/commands/device/purge.ts
Normal file
80
lib/commands/device/purge.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @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 type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DevicePurgeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Purge application data from a device.
|
||||
|
||||
Purge application data from a device.
|
||||
This will clear the application's /data directory.
|
||||
|
||||
Multiple devices may be specified with a comma-separated list
|
||||
of values (no spaces).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device purge 23c73a1',
|
||||
'$ balena device purge 55d43b3,23c73a1',
|
||||
];
|
||||
|
||||
public static usage = 'device purge <uuid>';
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const deviceIds = params.uuid.split(',').map((id) => {
|
||||
return tryAsInteger(id);
|
||||
});
|
||||
|
||||
for (const deviceId of deviceIds) {
|
||||
ux.action.start(`Purging data from device ${deviceId}`);
|
||||
await balena.models.device.purge(deviceId);
|
||||
ux.action.stop();
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
uuid?: string;
|
||||
@ -46,7 +45,6 @@ export default class DeviceRegisterCmd extends Command {
|
||||
{
|
||||
name: 'application',
|
||||
description: 'the name or id of application to register device with',
|
||||
parse: (app) => tryAsInteger(app),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
@ -68,9 +66,11 @@ export default class DeviceRegisterCmd extends Command {
|
||||
DeviceRegisterCmd,
|
||||
);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const application = await balena.models.application.get(params.application);
|
||||
const application = await getApplication(balena, params.application);
|
||||
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
|
||||
|
||||
console.info(`Registering to ${application.app_name}: ${uuid}`);
|
197
lib/commands/device/restart.ts
Normal file
197
lib/commands/device/restart.ts
Normal file
@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @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 type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||
import type {
|
||||
BalenaSDK,
|
||||
DeviceWithServiceDetails,
|
||||
CurrentServiceWithCommit,
|
||||
} from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart containers on a device.
|
||||
|
||||
Restart containers on a device.
|
||||
If the --service flag is provided, then only those services' containers
|
||||
will be restarted, otherwise all containers on the device will be restarted.
|
||||
|
||||
Multiple devices and services may be specified with a comma-separated list
|
||||
of values (no spaces).
|
||||
|
||||
Note this does not reboot the device, to do so use instead \`balena device reboot\`.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device restart 23c73a1',
|
||||
'$ balena device restart 55d43b3,23c73a1',
|
||||
'$ balena device restart 23c73a1 --service myService',
|
||||
'$ balena device restart 23c73a1 -s myService1,myService2',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description:
|
||||
'comma-separated list (no blank spaces) of device UUIDs to restart',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device restart <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
service: flags.string({
|
||||
description:
|
||||
'comma-separated list (no blank spaces) of service names to restart',
|
||||
char: 's',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceRestartCmd,
|
||||
);
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const deviceIds = params.uuid.split(',').map((id) => {
|
||||
return tryAsInteger(id);
|
||||
});
|
||||
const serviceNames = options.service?.split(',');
|
||||
|
||||
// Iterate sequentially through deviceIds.
|
||||
// We may later want to add a batching feature,
|
||||
// so that n devices are processed in parallel
|
||||
for (const deviceId of deviceIds) {
|
||||
ux.action.start(`Restarting services on device ${deviceId}`);
|
||||
if (serviceNames) {
|
||||
await this.restartServices(balena, deviceId, serviceNames);
|
||||
} else {
|
||||
await this.restartAllServices(balena, deviceId);
|
||||
}
|
||||
ux.action.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async restartServices(
|
||||
balena: BalenaSDK,
|
||||
deviceId: number | string,
|
||||
serviceNames: string[],
|
||||
) {
|
||||
const { ExpectedError, instanceOf } = await import('../../errors');
|
||||
const { getExpandedProp } = await import('../../utils/pine');
|
||||
|
||||
// Get device
|
||||
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
|
||||
try {
|
||||
device = await balena.models.device.getWithServiceDetails(deviceId, {
|
||||
$expand: {
|
||||
is_running__release: { $select: 'commit' },
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const activeRelease = getExpandedProp(device.is_running__release, 'commit');
|
||||
|
||||
// Check specified services exist on this device before restarting anything
|
||||
serviceNames.forEach((service) => {
|
||||
if (!device.current_services[service]) {
|
||||
throw new ExpectedError(
|
||||
`Service ${service} not found on device ${deviceId}.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Restart services
|
||||
const restartPromises: Array<Promise<void>> = [];
|
||||
for (const serviceName of serviceNames) {
|
||||
const service = device.current_services[serviceName];
|
||||
// Each service is an array of `CurrentServiceWithCommit`
|
||||
// because when service is updating, it will actually hold 2 services
|
||||
// Target commit matching `device.is_running__release`
|
||||
const serviceContainer = service.find((s) => {
|
||||
return s.commit === activeRelease;
|
||||
});
|
||||
|
||||
if (serviceContainer) {
|
||||
restartPromises.push(
|
||||
balena.models.device.restartService(
|
||||
deviceId,
|
||||
serviceContainer.image_id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(restartPromises);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('no online device')) {
|
||||
throw new ExpectedError(`Device ${deviceId} is not online.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restartAllServices(balena: BalenaSDK, deviceId: number | string) {
|
||||
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
|
||||
// Need to use device.get first to distinguish between non-existant and offline devices.
|
||||
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
|
||||
const { instanceOf, ExpectedError } = await import('../../errors');
|
||||
try {
|
||||
const device = await balena.models.device.get(deviceId);
|
||||
if (!device.is_online) {
|
||||
throw new ExpectedError(`Device ${deviceId} is not online.`);
|
||||
}
|
||||
} catch (e) {
|
||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await balena.models.device.restartApplication(deviceId);
|
||||
}
|
||||
}
|
@ -51,7 +51,6 @@ export default class DeviceRmCmd extends Command {
|
||||
name: 'uuid',
|
||||
description:
|
||||
'comma-separated list (no blank spaces) of device UUIDs to be removed',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
@ -83,9 +82,9 @@ export default class DeviceRmCmd extends Command {
|
||||
);
|
||||
|
||||
// Remove
|
||||
for (const uuid of params.uuid.split(',')) {
|
||||
for (const uuid of uuids) {
|
||||
try {
|
||||
await balena.models.device.remove(uuid);
|
||||
await balena.models.device.remove(tryAsInteger(uuid));
|
||||
} catch (err) {
|
||||
console.info(`${err.message}, uuid: ${uuid}`);
|
||||
process.exitCode = 1;
|
@ -25,7 +25,8 @@ import type { Application } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
application_name?: string;
|
||||
application_name?: string | null;
|
||||
device_type?: string | null;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
@ -45,8 +46,9 @@ export default class DevicesCmd extends Command {
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because field names are less likely to change in JSON format and because it
|
||||
better represents data types like arrays and empty strings. The 'jq' utility
|
||||
may also be helpful in shell scripts (https://stedolan.github.io/jq/manual/).
|
||||
better represents data types like arrays, empty strings and null values.
|
||||
The 'jq' utility may be helpful for querying JSON fields in shell scripts
|
||||
(https://stedolan.github.io/jq/manual/).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices',
|
||||
@ -97,14 +99,11 @@ export default class DevicesCmd extends Command {
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
device.application_name = belongsToApplication?.[0]?.app_name || null;
|
||||
|
||||
device.uuid = device.uuid.slice(0, 7);
|
||||
|
||||
// @ts-ignore
|
||||
device.device_type = device.is_of__device_type[0].slug;
|
||||
device.device_type = device.is_of__device_type?.[0]?.slug || null;
|
||||
return device;
|
||||
});
|
||||
|
||||
@ -120,8 +119,8 @@ export default class DevicesCmd extends Command {
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
const _ = await import('lodash');
|
||||
if (options.json) {
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
devices.map((device) => _.pick(device, fields)),
|
||||
@ -130,7 +129,12 @@ export default class DevicesCmd extends Command {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(getVisuals().table.horizontal(devices, fields));
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -76,11 +76,9 @@ export default class DevicesSupportedCmd extends Command {
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
||||
let deviceTypes: Array<Partial<
|
||||
SDK.DeviceTypeJson.DeviceType
|
||||
>> = await getBalenaSdk()
|
||||
.models.config.getDeviceTypes()
|
||||
.map((d) => {
|
||||
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);
|
||||
@ -93,7 +91,8 @@ export default class DevicesSupportedCmd extends Command {
|
||||
d.aliases = [];
|
||||
}
|
||||
return d;
|
||||
});
|
||||
},
|
||||
);
|
||||
if (!options.discontinued) {
|
||||
deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED');
|
||||
}
|
@ -22,7 +22,6 @@ import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string; // application name
|
||||
@ -39,17 +38,17 @@ interface ArgsDef {
|
||||
|
||||
export default class EnvAddCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Add an environment or config variable to one or more applications, devices or services.
|
||||
Add env or config variable to application(s), device(s) or service(s).
|
||||
|
||||
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
|
||||
used alongside the --service option to define a service-specific variable.
|
||||
(A service is an application container in a "microservices" application.)
|
||||
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
|
||||
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
|
||||
used alongside the --service option to define a service-specific variable.
|
||||
(A service is an application container in a "microservices" application.)
|
||||
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.
|
||||
|
||||
If VALUE is omitted, the CLI will attempt to use the value of the environment
|
||||
@ -91,9 +90,7 @@ export default class EnvAddCmd extends Command {
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
|
||||
public static usage =
|
||||
'env add ' + new CommandHelp({ args: EnvAddCmd.args }).defaultUsage();
|
||||
public static usage = 'env add <name> [value]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: { exclusive: ['device'], ...cf.application },
|
@ -20,7 +20,6 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ec from '../../utils/env-common';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
@ -70,9 +69,7 @@ export default class EnvRenameCmd extends Command {
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env rename' to avoid oclif's 'env:rename' topic syntax
|
||||
public static usage =
|
||||
'env rename ' + new CommandHelp({ args: EnvRenameCmd.args }).defaultUsage();
|
||||
public static usage = 'env rename <id> <value>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
config: ec.booleanConfig,
|
@ -20,7 +20,6 @@ import Command from '../../command';
|
||||
|
||||
import * as ec from '../../utils/env-common';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
@ -67,9 +66,7 @@ export default class EnvRmCmd extends Command {
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env rm' to avoid oclif's 'env:rm' topic syntax
|
||||
public static usage =
|
||||
'env rm ' + new CommandHelp({ args: EnvRmCmd.args }).defaultUsage();
|
||||
public static usage = 'env rm <id>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
config: ec.booleanConfig,
|
@ -40,7 +40,16 @@ export default class ScandevicesCmd extends Command {
|
||||
|
||||
public async run() {
|
||||
const { forms } = await import('balena-sync');
|
||||
const hostnameOrIp = await forms.selectLocalBalenaOsDevice();
|
||||
return console.error(`==> Selected device: ${hostnameOrIp}`);
|
||||
try {
|
||||
const hostnameOrIp = await forms.selectLocalBalenaOsDevice();
|
||||
return console.error(`==> Selected device: ${hostnameOrIp}`);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('could not find any')) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,9 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { parseAsLocalHostnameOrIp } from '../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
pollInterval?: number;
|
||||
help?: void;
|
||||
}
|
||||
|
||||
@ -61,6 +63,7 @@ export default class JoinCmd extends Command {
|
||||
{
|
||||
name: 'deviceIpOrHostname',
|
||||
description: 'the IP or hostname of device',
|
||||
parse: parseAsLocalHostnameOrIp,
|
||||
},
|
||||
];
|
||||
|
||||
@ -72,6 +75,10 @@ export default class JoinCmd extends Command {
|
||||
description: 'the name of the application the device should join',
|
||||
...cf.application,
|
||||
},
|
||||
pollInterval: flags.integer({
|
||||
description: 'the interval in minutes to check for updates',
|
||||
char: 'i',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -83,15 +90,15 @@ export default class JoinCmd extends Command {
|
||||
JoinCmd,
|
||||
);
|
||||
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = getBalenaSdk();
|
||||
const logger = Logger.getLogger();
|
||||
const logger = await Command.getLogger();
|
||||
return promote.join(
|
||||
logger,
|
||||
sdk,
|
||||
params.deviceIpOrHostname,
|
||||
options.application,
|
||||
options.pollInterval,
|
||||
);
|
||||
}
|
||||
}
|
@ -34,15 +34,30 @@ export default class KeyAddCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Add an SSH key to balenaCloud.
|
||||
|
||||
Register an SSH in balenaCloud for the logged in user.
|
||||
Add an SSH key to the balenaCloud account of the logged in user.
|
||||
|
||||
If \`path\` is omitted, the command will attempt
|
||||
to read the SSH key from stdin.
|
||||
If \`path\` is omitted, the command will attempt to read the SSH key from stdin.
|
||||
|
||||
About SSH keys
|
||||
An "SSH key" actually consists of a public/private key pair. A typical name
|
||||
for the private key file is "id_rsa", and a typical name for the public key
|
||||
file is "id_rsa.pub". Both key files are saved to your computer (with the
|
||||
private key optionally protected by a password), but only the public key is
|
||||
saved to your balena account. This means that if you change computers or
|
||||
otherwise lose the private key, you cannot recover the private key through
|
||||
your balena account. You can however add new keys, and delete the old ones.
|
||||
|
||||
To generate a new SSH key pair, a nice guide can be found in GitHub's docs:
|
||||
https://help.github.com/en/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent
|
||||
Skip the step about adding the key to a GitHub account, and instead add it to
|
||||
your balena account.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena key add Main ~/.ssh/id_rsa.pub',
|
||||
'$ cat ~/.ssh/id_rsa.pub | balena key add Main',
|
||||
'# Windows 10 (cmd.exe prompt) example',
|
||||
'$ balena key add Main %userprofile%.sshid_rsa.pub',
|
||||
];
|
||||
|
||||
public static args = [
|
@ -19,6 +19,7 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { parseAsLocalHostnameOrIp } from '../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help?: void;
|
||||
@ -54,10 +55,10 @@ export default class LeaveCmd extends Command {
|
||||
{
|
||||
name: 'deviceIpOrHostname',
|
||||
description: 'the device IP or hostname',
|
||||
parse: parseAsLocalHostnameOrIp,
|
||||
},
|
||||
];
|
||||
|
||||
// Hardcoded to preserve camelcase
|
||||
public static usage = 'leave [deviceIpOrHostname]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
@ -70,10 +71,9 @@ export default class LeaveCmd extends Command {
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(LeaveCmd);
|
||||
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = getBalenaSdk();
|
||||
const logger = Logger.getLogger();
|
||||
const logger = await Command.getLogger();
|
||||
return promote.leave(logger, sdk, params.deviceIpOrHostname);
|
||||
}
|
||||
}
|
@ -89,20 +89,17 @@ export default class LocalConfigureCmd extends Command {
|
||||
const dmHandler = (cb: () => void) =>
|
||||
reconfix
|
||||
.readConfiguration(configurationSchema, params.target)
|
||||
.tap((config: any) => {
|
||||
.then(async (config: any) => {
|
||||
logger.logDebug('Current config:');
|
||||
logger.logDebug(JSON.stringify(config));
|
||||
})
|
||||
.then((config: any) => this.getConfiguration(config))
|
||||
.tap((config: any) => {
|
||||
const answers = await this.getConfiguration(config);
|
||||
logger.logDebug('New config:');
|
||||
logger.logDebug(JSON.stringify(config));
|
||||
})
|
||||
.then(async (answers: any) => {
|
||||
logger.logDebug(JSON.stringify(answers));
|
||||
|
||||
if (!answers.hostname) {
|
||||
await this.removeHostname(configurationSchema);
|
||||
}
|
||||
return reconfix.writeConfiguration(
|
||||
return await reconfix.writeConfiguration(
|
||||
configurationSchema,
|
||||
answers,
|
||||
params.target,
|
||||
@ -119,9 +116,7 @@ export default class LocalConfigureCmd extends Command {
|
||||
readonly CONNECTIONS_FOLDER = '/system-connections';
|
||||
|
||||
getConfigurationSchema(connectionFileName?: string) {
|
||||
if (connectionFileName == null) {
|
||||
connectionFileName = 'resin-wifi';
|
||||
}
|
||||
connectionFileName ??= 'resin-wifi';
|
||||
return {
|
||||
mapper: [
|
||||
{
|
||||
@ -222,9 +217,8 @@ export default class LocalConfigureCmd extends Command {
|
||||
persistentLogging: data.persistentLogging || false,
|
||||
});
|
||||
|
||||
return inquirer
|
||||
.prompt(this.inquirerOptions(data))
|
||||
.then((answers: any) => _.merge(data, answers));
|
||||
const answers = await inquirer.prompt(this.inquirerOptions(data));
|
||||
return _.merge(data, answers);
|
||||
};
|
||||
|
||||
// Taken from https://goo.gl/kr1kCt
|
||||
@ -261,62 +255,50 @@ export default class LocalConfigureCmd extends Command {
|
||||
const _ = await import('lodash');
|
||||
const imagefs = await import('resin-image-fs');
|
||||
|
||||
return imagefs
|
||||
.listDirectory({
|
||||
image: target,
|
||||
partition: this.BOOT_PARTITION,
|
||||
path: this.CONNECTIONS_FOLDER,
|
||||
})
|
||||
.then((files: string[]) => {
|
||||
// The required file already exists
|
||||
if (_.includes(files, 'resin-wifi')) {
|
||||
return null;
|
||||
}
|
||||
const files = await imagefs.listDirectory({
|
||||
image: target,
|
||||
partition: this.BOOT_PARTITION,
|
||||
path: this.CONNECTIONS_FOLDER,
|
||||
});
|
||||
|
||||
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||
if (_.includes(files, 'resin-sample.ignore')) {
|
||||
return 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`,
|
||||
},
|
||||
)
|
||||
.thenReturn(null);
|
||||
}
|
||||
|
||||
// Legacy mode, to be removed later
|
||||
// We return the file name override from this branch
|
||||
// When it is removed the following cleanup should be done:
|
||||
// * delete all the null returns from this method
|
||||
// * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi`
|
||||
// * drop the final `then` from this method
|
||||
// * adapt the code in the main listener to not receive the config from this method, and use that constant instead
|
||||
if (_.includes(files, 'resin-sample')) {
|
||||
return 'resin-sample';
|
||||
}
|
||||
|
||||
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
||||
return imagefs
|
||||
.writeFile(
|
||||
{
|
||||
image: target,
|
||||
partition: this.BOOT_PARTITION,
|
||||
path: `${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
},
|
||||
this.CONNECTION_FILE,
|
||||
)
|
||||
.thenReturn(null);
|
||||
})
|
||||
.then((connectionFileName) =>
|
||||
this.getConfigurationSchema(connectionFileName || undefined),
|
||||
let connectionFileName;
|
||||
if (_.includes(files, 'resin-wifi')) {
|
||||
// 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`,
|
||||
},
|
||||
);
|
||||
} else if (_.includes(files, 'resin-sample')) {
|
||||
// Legacy mode, to be removed later
|
||||
// We return the file name override from this branch
|
||||
// When it is removed the following cleanup should be done:
|
||||
// * delete all the null returns from this method
|
||||
// * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi`
|
||||
// * drop the final `then` from this method
|
||||
// * adapt the code in the main listener to not receive the config from this method, and use that constant instead
|
||||
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,
|
||||
);
|
||||
}
|
||||
return await this.getConfigurationSchema(connectionFileName);
|
||||
}
|
||||
|
||||
async removeHostname(schema: any) {
|
@ -132,7 +132,7 @@ export default class LoginCmd extends Command {
|
||||
|
||||
console.log(messages.balenaAsciiArt);
|
||||
console.log(`\nLogging in to ${balenaUrl}`);
|
||||
await this.doLogin(options, params.token);
|
||||
await this.doLogin(options, balenaUrl, params.token);
|
||||
|
||||
const username = await balena.auth.whoami();
|
||||
|
||||
@ -146,7 +146,11 @@ Find out about the available commands by running:
|
||||
${messages.reachingOut}`);
|
||||
}
|
||||
|
||||
async doLogin(loginOptions: FlagsDef, token?: string): Promise<void> {
|
||||
async doLogin(
|
||||
loginOptions: FlagsDef,
|
||||
balenaUrl: string = 'balena-cloud.com',
|
||||
token?: string,
|
||||
): Promise<void> {
|
||||
// Token
|
||||
if (loginOptions.token) {
|
||||
if (!token) {
|
||||
@ -178,8 +182,8 @@ ${messages.reachingOut}`);
|
||||
// User had not selected login preference, prompt interactively
|
||||
const loginType = await patterns.askLoginType();
|
||||
if (loginType === 'register') {
|
||||
const signupUrl = 'https://dashboard.balena-cloud.com/signup';
|
||||
const open = await import('open');
|
||||
const signupUrl = `https://dashboard.${balenaUrl}/signup`;
|
||||
open(signupUrl, { wait: false });
|
||||
throw new ExpectedError(`Please sign up at ${signupUrl}`);
|
||||
}
|
@ -115,10 +115,8 @@ export default class LogsCmd extends Command {
|
||||
|
||||
const displayCloudLog = async (line: LogMessage) => {
|
||||
if (!line.isSystem) {
|
||||
let serviceName = await serviceIdToName(balena, line.serviceId);
|
||||
if (serviceName == null) {
|
||||
serviceName = 'Unknown service';
|
||||
}
|
||||
const serviceName =
|
||||
(await serviceIdToName(balena, line.serviceId)) ?? 'Unknown service';
|
||||
displayLogObject(
|
||||
{ serviceName, ...line },
|
||||
logger,
|
@ -107,9 +107,7 @@ export default class OsBuildConfigCmd extends Command {
|
||||
deviceTypeManifest: DeviceTypeJson.DeviceType,
|
||||
advanced: boolean,
|
||||
) {
|
||||
if (advanced == null) {
|
||||
advanced = false;
|
||||
}
|
||||
advanced ??= false;
|
||||
|
||||
let override;
|
||||
const questions = deviceTypeManifest.options;
|
@ -24,7 +24,6 @@ import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
const BOOT_PARTITION = 1;
|
||||
const CONNECTIONS_FOLDER = '/system-connections';
|
||||
@ -111,10 +110,7 @@ export default class OsConfigureCmd extends Command {
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'os configure' to avoid oclif's 'os:configure' topic syntax
|
||||
public static usage =
|
||||
'os configure ' +
|
||||
new CommandHelp({ args: OsConfigureCmd.args }).defaultUsage();
|
||||
public static usage = 'os configure <image>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
advanced: flags.boolean({
|
||||
@ -188,6 +184,8 @@ export default class OsConfigureCmd extends Command {
|
||||
'../../utils/config'
|
||||
);
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
let app: ApplicationWithDeviceType | undefined;
|
||||
let device;
|
||||
let deviceTypeSlug: string;
|
||||
@ -203,7 +201,7 @@ export default class OsConfigureCmd extends Command {
|
||||
};
|
||||
deviceTypeSlug = device.is_of__device_type[0].slug;
|
||||
} else {
|
||||
app = (await balena.models.application.get(options.application!, {
|
||||
app = (await getApplication(balena, options.application!, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
@ -46,11 +46,15 @@ export default class OsDownloadCmd extends Command {
|
||||
|
||||
You can pass \`--version menu\` to pick the OS version from the interactive menu
|
||||
of all available versions.
|
||||
|
||||
To download a development image append \`.dev\` to the version or select from
|
||||
the interactive menu.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 1.24.1',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^1.20.0',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu',
|
@ -42,6 +42,7 @@ export default class OsVersionsCmd extends Command {
|
||||
{
|
||||
name: 'type',
|
||||
description: 'device type',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
530
lib/commands/preload.ts
Normal file
530
lib/commands/preload.ts
Normal file
@ -0,0 +1,530 @@
|
||||
/**
|
||||
* @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 {
|
||||
getBalenaSdk,
|
||||
getCliForm,
|
||||
getVisuals,
|
||||
stripIndent,
|
||||
} from '../utils/lazy';
|
||||
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';
|
||||
|
||||
interface FlagsDef extends DockerConnectionCliFlags {
|
||||
app?: string;
|
||||
commit?: string;
|
||||
'splash-image'?: string;
|
||||
'dont-check-arch': boolean;
|
||||
'pin-device-to-release': boolean;
|
||||
'add-certificate'?: string[];
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
image: string;
|
||||
}
|
||||
|
||||
export default class PreloadCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Preload an app on a disk image (or Edison zip archive).
|
||||
|
||||
Preload a balena application release (app images/containers), 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.
|
||||
|
||||
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
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png',
|
||||
'$ balena preload balena.img',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'image',
|
||||
description: 'the image file path',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'preload <image>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
app: flags.string({
|
||||
description: 'name of the application to preload',
|
||||
char: 'a',
|
||||
}),
|
||||
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 .\
|
||||
`,
|
||||
char: 'c',
|
||||
}),
|
||||
'splash-image': flags.string({
|
||||
description: 'path to a png image to replace the splash screen',
|
||||
char: 's',
|
||||
}),
|
||||
'dont-check-arch': flags.boolean({
|
||||
description:
|
||||
'disables check for matching architecture in image and application',
|
||||
}),
|
||||
'pin-device-to-release': flags.boolean({
|
||||
description:
|
||||
'pin the preloaded device to the preloaded release on provision',
|
||||
char: 'p',
|
||||
}),
|
||||
'add-certificate': flags.string({
|
||||
description: `\
|
||||
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
|
||||
The file name must end with '.crt' and must not be already contained in the preloader's
|
||||
/etc/ssl/certs folder.
|
||||
Can be repeated to add multiple certificates.\
|
||||
`,
|
||||
multiple: true,
|
||||
}),
|
||||
...dockerConnectionCliFlags,
|
||||
// Redefining --dockerPort here (defined already in dockerConnectionCliFlags)
|
||||
// without -p alias, to avoid clash with -p alias of pin-device-to-release
|
||||
dockerPort: flags.integer({
|
||||
description:
|
||||
'Docker daemon TCP port number (hint: 2375 for balena devices)',
|
||||
parse: (p) => parseAsInteger(p, 'dockerPort'),
|
||||
}),
|
||||
// Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
// Revisit this in future release.
|
||||
help: flags.help({}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
PreloadCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const balenaPreload = await import('balena-preload');
|
||||
const visuals = getVisuals();
|
||||
const nodeCleanup = await import('node-cleanup');
|
||||
const { instanceOf } = await import('../errors');
|
||||
|
||||
// Check image file exists
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
await fs.promises.access(params.image);
|
||||
} catch (error) {
|
||||
throw new ExpectedError(
|
||||
`The provided image path does not exist: ${params.image}`,
|
||||
);
|
||||
}
|
||||
|
||||
const progressBars: {
|
||||
[key: string]: ReturnType<typeof getVisuals>['Progress'];
|
||||
} = {};
|
||||
|
||||
const progressHandler = function (event: {
|
||||
name: string;
|
||||
percentage: number;
|
||||
}) {
|
||||
const progressBar = (progressBars[event.name] ??= new visuals.Progress(
|
||||
event.name,
|
||||
));
|
||||
return progressBar.update({ percentage: event.percentage });
|
||||
};
|
||||
|
||||
const spinners: {
|
||||
[key: string]: ReturnType<typeof getVisuals>['Spinner'];
|
||||
} = {};
|
||||
|
||||
const spinnerHandler = function (event: { name: string; action: string }) {
|
||||
const spinner = (spinners[event.name] ??= new visuals.Spinner(
|
||||
event.name,
|
||||
));
|
||||
if (event.action === 'start') {
|
||||
return spinner.start();
|
||||
} else {
|
||||
console.log();
|
||||
return spinner.stop();
|
||||
}
|
||||
};
|
||||
|
||||
const commit = this.isCurrentCommit(options.commit || '')
|
||||
? 'latest'
|
||||
: options.commit;
|
||||
const image = params.image;
|
||||
const appId = options.app;
|
||||
|
||||
const splashImage = options['splash-image'];
|
||||
|
||||
const dontCheckArch = options['dont-check-arch'] || false;
|
||||
const pinDevice = options['pin-device-to-release'] || false;
|
||||
|
||||
if (dontCheckArch && !appId) {
|
||||
throw new ExpectedError(
|
||||
'You need to specify an app id if you disable the architecture check.',
|
||||
);
|
||||
}
|
||||
|
||||
const certificates: string[] = options['add-certificate'] || [];
|
||||
for (const certificate of certificates) {
|
||||
if (!certificate.endsWith('.crt')) {
|
||||
throw new ExpectedError('Certificate file name must end with ".crt"');
|
||||
}
|
||||
}
|
||||
|
||||
// Get a configured dockerode instance
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
const docker = await dockerUtils.getDocker(options);
|
||||
const preloader = new balenaPreload.Preloader(
|
||||
null,
|
||||
docker,
|
||||
appId,
|
||||
commit,
|
||||
image,
|
||||
splashImage,
|
||||
undefined, // TODO: Currently always undefined, investigate approach in ssh command.
|
||||
dontCheckArch,
|
||||
pinDevice,
|
||||
certificates,
|
||||
);
|
||||
|
||||
let gotSignal = false;
|
||||
|
||||
nodeCleanup(function (_exitCode, signal) {
|
||||
if (signal) {
|
||||
gotSignal = true;
|
||||
nodeCleanup.uninstall(); // don't call cleanup handler again
|
||||
preloader.cleanup().then(() => {
|
||||
// calling process.exit() won't inform parent process of signal
|
||||
process.kill(process.pid, signal);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
preloader.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
preloader.on('progress', progressHandler);
|
||||
preloader.on('spinner', spinnerHandler);
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
preloader.on('error', reject);
|
||||
resolve(
|
||||
this.prepareAndPreload(preloader, balena, {
|
||||
appId,
|
||||
commit,
|
||||
pinDevice,
|
||||
}),
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
if (instanceOf(err, balena.errors.BalenaError)) {
|
||||
const code = err.code ? `(${err.code})` : '';
|
||||
throw new ExpectedError(`${err.message} ${code}`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (!gotSignal) {
|
||||
await preloader.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly applicationExpandOptions: PineExpand<Application> = {
|
||||
owns__release: {
|
||||
$select: ['id', 'commit', 'end_timestamp', 'composition'],
|
||||
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
|
||||
$expand: {
|
||||
contains__image: {
|
||||
$select: ['image'],
|
||||
$expand: {
|
||||
image: {
|
||||
$select: ['image_size', 'is_stored_at__image_location'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
$filter: {
|
||||
status: 'success',
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
$expand: this.applicationExpandOptions,
|
||||
$select: ['id', 'app_name', 'should_track_latest_release'],
|
||||
$orderby: 'app_name asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async selectApplication(deviceTypeSlug: string) {
|
||||
const visuals = getVisuals();
|
||||
|
||||
const applicationInfoSpinner = new visuals.Spinner(
|
||||
'Downloading list of applications and releases.',
|
||||
);
|
||||
applicationInfoSpinner.start();
|
||||
|
||||
const applications = await this.getApplicationsWithSuccessfulBuilds(
|
||||
deviceTypeSlug,
|
||||
);
|
||||
applicationInfoSpinner.stop();
|
||||
if (applications.length === 0) {
|
||||
throw new ExpectedError(
|
||||
`You have no apps with successful releases for a '${deviceTypeSlug}' device type.`,
|
||||
);
|
||||
}
|
||||
return getCliForm().ask({
|
||||
message: 'Select an application',
|
||||
type: 'list',
|
||||
choices: applications.map((app) => ({
|
||||
name: app.app_name,
|
||||
value: app,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
selectApplicationCommit(releases: Release[]) {
|
||||
if (releases.length === 0) {
|
||||
throw new ExpectedError('This application has no successful releases.');
|
||||
}
|
||||
const DEFAULT_CHOICE = { name: 'current', value: 'current' };
|
||||
const choices = [DEFAULT_CHOICE].concat(
|
||||
releases.map((release) => ({
|
||||
name: `${release.end_timestamp} - ${release.commit}`,
|
||||
value: release.commit,
|
||||
})),
|
||||
);
|
||||
return getCliForm().ask({
|
||||
message: 'Select a release',
|
||||
type: 'list',
|
||||
default: 'current',
|
||||
choices,
|
||||
});
|
||||
}
|
||||
|
||||
async offerToDisableAutomaticUpdates(
|
||||
application: Application,
|
||||
commit: string,
|
||||
pinDevice: boolean,
|
||||
) {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if (
|
||||
this.isCurrentCommit(commit) ||
|
||||
!application.should_track_latest_release ||
|
||||
pinDevice
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const message = `\
|
||||
|
||||
This application 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
|
||||
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:
|
||||
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?\
|
||||
`;
|
||||
const update = await getCliForm().ask({
|
||||
message,
|
||||
type: 'confirm',
|
||||
});
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
return await balena.pine.patch({
|
||||
resource: 'application',
|
||||
id: application.id,
|
||||
body: {
|
||||
should_track_latest_release: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) {
|
||||
const { getApplication } = await import('../utils/sdk');
|
||||
|
||||
return (await getApplication(balenaSdk, appId, {
|
||||
$expand: this.applicationExpandOptions,
|
||||
})) as Application & { should_be_running__release: [Release?] };
|
||||
}
|
||||
|
||||
async prepareAndPreload(
|
||||
preloader: Preloader,
|
||||
balenaSdk: BalenaSDK,
|
||||
options: {
|
||||
appId?: string;
|
||||
commit?: string;
|
||||
pinDevice: boolean;
|
||||
},
|
||||
) {
|
||||
await preloader.prepare();
|
||||
|
||||
const application = options.appId
|
||||
? await this.getAppWithReleases(balenaSdk, options.appId)
|
||||
: await this.selectApplication(preloader.config.deviceType);
|
||||
|
||||
let commit: string; // commit hash or the strings 'latest' or 'current'
|
||||
|
||||
const appCommit = application.should_be_running__release[0]?.commit;
|
||||
|
||||
// Use the commit given as --commit or show an interactive commit selection menu
|
||||
if (options.commit) {
|
||||
if (this.isCurrentCommit(options.commit)) {
|
||||
if (!appCommit) {
|
||||
throw new Error(
|
||||
`Unexpected empty commit hash for app ID "${application.id}"`,
|
||||
);
|
||||
}
|
||||
// handle `--commit current` (and its `--commit latest` synonym)
|
||||
commit = 'latest';
|
||||
} else {
|
||||
const release = _.find(application.owns__release, (r) =>
|
||||
r.commit.startsWith(options.commit!),
|
||||
);
|
||||
if (!release) {
|
||||
throw new ExpectedError(
|
||||
`There is no release matching commit "${options.commit}"`,
|
||||
);
|
||||
}
|
||||
commit = release.commit;
|
||||
}
|
||||
} else {
|
||||
// this could have the value 'current'
|
||||
commit = await this.selectApplicationCommit(
|
||||
application.owns__release as Release[],
|
||||
);
|
||||
}
|
||||
|
||||
await preloader.setAppIdAndCommit(
|
||||
application.id,
|
||||
this.isCurrentCommit(commit) ? appCommit! : commit,
|
||||
);
|
||||
|
||||
// Propose to disable automatic app updates if the commit is not the current release
|
||||
await this.offerToDisableAutomaticUpdates(
|
||||
application,
|
||||
commit,
|
||||
options.pinDevice,
|
||||
);
|
||||
|
||||
// All options are ready: preload the image.
|
||||
await preloader.preload();
|
||||
}
|
||||
}
|
@ -33,6 +33,7 @@ interface FlagsDef {
|
||||
emulated: boolean;
|
||||
dockerfile?: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
|
||||
nocache?: boolean;
|
||||
pull?: boolean;
|
||||
'noparent-check'?: boolean;
|
||||
'registry-secrets'?: string;
|
||||
gitignore?: boolean;
|
||||
@ -54,10 +55,9 @@ interface ArgsDef {
|
||||
|
||||
export default class PushCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Start a remote build on the balena cloud build servers or a local mode device.
|
||||
Start a remote build on the balenaCloud build servers or a local mode device.
|
||||
|
||||
start a build on the remote balena cloud builders,
|
||||
or a local mode balena device.
|
||||
Start a build on the remote balenaCloud builders, or a local mode balena 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
|
||||
@ -84,8 +84,8 @@ export default class PushCmd extends Command {
|
||||
|
||||
${dockerignoreHelp.split('\n').join('\n\t\t')}
|
||||
|
||||
Note: --service and --env flags must come after the applicationOrDevice parameter,
|
||||
as per examples.
|
||||
Note: the --service and --env flags must come after the applicationOrDevice
|
||||
parameter, as per examples.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
@ -115,13 +115,16 @@ export default class PushCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
source: flags.string({
|
||||
description:
|
||||
'Source directory to be sent to balenaCloud or balenaOS device (default: current working dir)',
|
||||
description: stripIndent`
|
||||
Source directory to be sent to balenaCloud or balenaOS device
|
||||
(default: current working dir)`,
|
||||
char: 's',
|
||||
}),
|
||||
emulated: flags.boolean({
|
||||
description: 'Force an emulated build to occur on the remote builder',
|
||||
char: 'f',
|
||||
description: stripIndent`
|
||||
Don't use native ARM servers; force QEMU ARM emulation on Intel x86-64
|
||||
servers during the image build (balenaCloud).`,
|
||||
char: 'e',
|
||||
}),
|
||||
dockerfile: flags.string({
|
||||
description:
|
||||
@ -129,38 +132,44 @@ export default class PushCmd extends Command {
|
||||
}),
|
||||
nocache: flags.boolean({
|
||||
description: stripIndent`
|
||||
Don't use cached layers of previously built images for this project. This ensures
|
||||
that the latest base image and packages are pulled. Note that build logs may still
|
||||
display the message _"Pulling previous images for caching purposes" (as the cloud
|
||||
builder needs previous images to compute delta updates), but the logs will not
|
||||
display the "Using cache" lines for each build step of a Dockerfile.`,
|
||||
|
||||
Don't use cached layers of previously built images for this project. This
|
||||
ensures that the latest base image and packages are pulled. Note that build
|
||||
logs may still display the message _"Pulling previous images for caching
|
||||
purposes" (as the cloud builder needs previous images to compute delta
|
||||
updates), but the logs will not display the "Using cache" lines for each
|
||||
build step of a Dockerfile.`,
|
||||
char: 'c',
|
||||
}),
|
||||
pull: flags.boolean({
|
||||
description: stripIndent`
|
||||
When pushing to a local device, force the base images to be pulled again.
|
||||
Currently this option is ignored when pushing to the balenaCloud builders.`,
|
||||
}),
|
||||
'noparent-check': flags.boolean({
|
||||
description: `Disable project validation check of 'docker-compose.yml' file in parent folder`,
|
||||
description: stripIndent`
|
||||
Disable project validation check of 'docker-compose.yml' file in parent folder`,
|
||||
}),
|
||||
'registry-secrets': flags.string({
|
||||
description: stripIndent`
|
||||
Path to a local YAML or JSON file containing Docker registry passwords used to pull base images.
|
||||
Note that if registry-secrets are not provided on the command line, a secrets configuration
|
||||
file from the balena directory will be used (usually $HOME/.balena/secrets.yml|.json)`,
|
||||
Path to a local YAML or JSON file containing Docker registry passwords used
|
||||
to pull base images. Note that if registry-secrets are not provided on the
|
||||
command line, a secrets configuration file from the balena directory will be
|
||||
used (usually $HOME/.balena/secrets.yml|.json)`,
|
||||
char: 'R',
|
||||
}),
|
||||
nolive: flags.boolean({
|
||||
description: stripIndent`
|
||||
Don't run a live session on this push. The filesystem will not be monitored, and changes
|
||||
will not be synchronized to any running containers. Note that both this flag and --detached
|
||||
and required to cause the process to end once the initial build has completed.`,
|
||||
Don't run a live session on this push. The filesystem will not be monitored,
|
||||
and changes will not be synchronized to any running containers. Note that both
|
||||
this flag and --detached and required to cause the process to end once the
|
||||
initial build has completed.`,
|
||||
}),
|
||||
detached: flags.boolean({
|
||||
description: stripIndent`
|
||||
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.`,
|
||||
|
||||
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.`,
|
||||
char: 'd',
|
||||
}),
|
||||
service: flags.string({
|
||||
@ -210,8 +219,8 @@ export default class PushCmd extends Command {
|
||||
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.`,
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is
|
||||
required until your project can be adapted.`,
|
||||
char: 'g',
|
||||
exclusive: ['multi-dockerignore'],
|
||||
}),
|
||||
@ -308,6 +317,7 @@ export default class PushCmd extends Command {
|
||||
registrySecrets,
|
||||
multiDockerignore: options['multi-dockerignore'] || false,
|
||||
nocache: options.nocache || false,
|
||||
pull: options.pull || false,
|
||||
nogitignore,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
nolive: options.nolive || false,
|
||||
@ -328,15 +338,9 @@ export default class PushCmd extends Command {
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ExpectedError(
|
||||
stripIndent`
|
||||
Build target not recognized. Please provide either an application name or device address.
|
||||
|
||||
The only supported device addresses currently are IP addresses.
|
||||
|
||||
If you believe your build target should have been detected, and this is an error, please
|
||||
create an issue.`,
|
||||
);
|
||||
throw new ExpectedError(stripIndent`
|
||||
Build target not recognized. Please provide either an application name or
|
||||
device IP address.`);
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,10 @@ import { flags } from '@oclif/command';
|
||||
import type { LocalBalenaOsDevice } from 'balena-sync';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { getCliUx, stripIndent } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
json?: boolean;
|
||||
verbose: boolean;
|
||||
timeout?: number;
|
||||
help: void;
|
||||
@ -32,6 +33,11 @@ export default class ScanCmd extends Command {
|
||||
Scan for balenaOS devices on your local network.
|
||||
|
||||
Scan for balenaOS devices on your local network.
|
||||
|
||||
The output includes device information collected through balenaEngine for
|
||||
devices running a development image of balenaOS. Devices running a production
|
||||
image do not expose balenaEngine (on TCP port 2375), which is why less
|
||||
information is printed about them.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
@ -53,6 +59,10 @@ export default class ScanCmd extends Command {
|
||||
description: 'scan timeout in seconds',
|
||||
}),
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
@ -60,10 +70,8 @@ export default class ScanCmd extends Command {
|
||||
|
||||
public async run() {
|
||||
const _ = await import('lodash');
|
||||
const { SpinnerPromise } = getVisuals();
|
||||
const { discover } = await import('balena-sync');
|
||||
const prettyjson = await import('prettyjson');
|
||||
const { ExpectedError } = await import('../errors');
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
|
||||
const dockerPort = 2375;
|
||||
@ -75,37 +83,54 @@ export default class ScanCmd extends Command {
|
||||
options.timeout != null ? options.timeout * 1000 : undefined;
|
||||
|
||||
// Find active local devices
|
||||
const activeLocalDevices: LocalBalenaOsDevice[] = await new SpinnerPromise({
|
||||
promise: discover.discoverLocalBalenaOsDevices(discoverTimeout),
|
||||
startMessage: 'Scanning for local balenaOS devices..',
|
||||
stopMessage: 'Reporting scan results',
|
||||
}).filter(async ({ address }: { address: string }) => {
|
||||
const docker = dockerUtils.createClient({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
}) as any;
|
||||
try {
|
||||
await docker.pingAsync();
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const ux = getCliUx();
|
||||
ux.action.start('Scanning for local balenaOS devices');
|
||||
|
||||
// Exit with message if no devices found
|
||||
if (_.isEmpty(activeLocalDevices)) {
|
||||
// TODO: Consider whether this should really be an error
|
||||
throw new ExpectedError(
|
||||
process.platform === 'win32'
|
||||
? ScanCmd.noDevicesFoundMessage + ScanCmd.windowsTipMessage
|
||||
: ScanCmd.noDevicesFoundMessage,
|
||||
);
|
||||
}
|
||||
const localDevices: LocalBalenaOsDevice[] = await discover.discoverLocalBalenaOsDevices(
|
||||
discoverTimeout,
|
||||
);
|
||||
const engineReachableDevices: boolean[] = await Promise.all(
|
||||
localDevices.map(async ({ address }: { address: string }) => {
|
||||
const docker = dockerUtils.createClient({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
}) as any;
|
||||
try {
|
||||
await docker.pingAsync();
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const developmentDevices: LocalBalenaOsDevice[] = localDevices.filter(
|
||||
(_localDevice, index) => engineReachableDevices[index],
|
||||
);
|
||||
|
||||
const productionDevices = _.differenceWith(
|
||||
localDevices,
|
||||
developmentDevices,
|
||||
_.isEqual,
|
||||
);
|
||||
|
||||
const productionDevicesInfo = _.map(
|
||||
productionDevices,
|
||||
(device: LocalBalenaOsDevice) => {
|
||||
return {
|
||||
host: device.host,
|
||||
address: device.address,
|
||||
osVariant: 'production',
|
||||
dockerInfo: undefined,
|
||||
dockerVersion: undefined,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Query devices for info
|
||||
const devicesInfo = await Promise.all(
|
||||
activeLocalDevices.map(async ({ host, address }) => {
|
||||
developmentDevices.map(async ({ host, address }) => {
|
||||
const docker = dockerUtils.createClient({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
@ -118,12 +143,15 @@ export default class ScanCmd extends Command {
|
||||
return {
|
||||
host,
|
||||
address,
|
||||
osVariant: 'development',
|
||||
dockerInfo,
|
||||
dockerVersion,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
ux.action.stop('Reporting scan results');
|
||||
|
||||
// Reduce properties if not --verbose
|
||||
if (!options.verbose) {
|
||||
devicesInfo.forEach((d: any) => {
|
||||
@ -136,8 +164,22 @@ export default class ScanCmd extends Command {
|
||||
});
|
||||
}
|
||||
|
||||
const cmdOutput = productionDevicesInfo.concat(devicesInfo);
|
||||
|
||||
// Output results
|
||||
console.log(prettyjson.render(devicesInfo, { noColor: true }));
|
||||
if (!options.json && cmdOutput.length === 0) {
|
||||
console.error(
|
||||
process.platform === 'win32'
|
||||
? ScanCmd.noDevicesFoundMessage + ScanCmd.windowsTipMessage
|
||||
: ScanCmd.noDevicesFoundMessage,
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
options.json
|
||||
? JSON.stringify(cmdOutput, null, 4)
|
||||
: prettyjson.render(cmdOutput, { noColor: true }),
|
||||
);
|
||||
}
|
||||
|
||||
protected static dockerInfoProperties = [
|
@ -28,7 +28,7 @@ export default class SettingsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Print current settings.
|
||||
|
||||
Use this command to display current balena CLI settings.
|
||||
Use this command to display the current balena CLI settings.
|
||||
`;
|
||||
public static examples = ['$ balena settings'];
|
||||
|
@ -36,7 +36,7 @@ interface FlagsDef {
|
||||
|
||||
interface ArgsDef {
|
||||
applicationOrDevice: string;
|
||||
serviceName?: string;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
export default class NoteCmd extends Command {
|
||||
@ -85,13 +85,13 @@ export default class NoteCmd extends Command {
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'serviceName',
|
||||
name: 'service',
|
||||
description: 'service name, if connecting to a container',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'ssh <applicationOrDevice> [serviceName]';
|
||||
public static usage = 'ssh <applicationOrDevice> [service]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
port: flags.integer({
|
||||
@ -134,7 +134,7 @@ export default class NoteCmd extends Command {
|
||||
port: options.port,
|
||||
forceTTY: options.tty,
|
||||
verbose: options.verbose,
|
||||
service: params.serviceName,
|
||||
service: params.service,
|
||||
});
|
||||
}
|
||||
|
||||
@ -214,11 +214,11 @@ export default class NoteCmd extends Command {
|
||||
// At this point, we have a long uuid with a device
|
||||
// that we know exists and is accessible
|
||||
let containerId: string | undefined;
|
||||
if (params.serviceName != null) {
|
||||
if (params.service != null) {
|
||||
containerId = await this.getContainerId(
|
||||
sdk,
|
||||
uuid,
|
||||
params.serviceName,
|
||||
params.service,
|
||||
{
|
||||
port: options.port,
|
||||
proxyCommand,
|
173
lib/commands/support.ts
Normal file
173
lib/commands/support.ts
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* @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 { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
duration?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
action: string;
|
||||
}
|
||||
|
||||
export default class SupportCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Grant or revoke support access for devices and applications.
|
||||
|
||||
Grant or revoke balena support agent access to devices and applications
|
||||
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
|
||||
a comma-separated list (with no spaces).
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'balena support enable --device ab346f,cd457a --duration 3d',
|
||||
'balena support enable --application app3 --duration 12h',
|
||||
'balena support disable -a myApp',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'action',
|
||||
description: 'enable|disable support access',
|
||||
options: ['enable', 'disable'],
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'support <action>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
device: flags.string({
|
||||
description: 'comma-separated list (no spaces) of device UUIDs',
|
||||
char: 'd',
|
||||
}),
|
||||
application: flags.string({
|
||||
description: 'comma-separated list (no spaces) of application names',
|
||||
char: 'a',
|
||||
}),
|
||||
duration: flags.string({
|
||||
description:
|
||||
'length of time to enable support for, in (h)ours or (d)ays, e.g. 12h, 2d',
|
||||
char: 't',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
SupportCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const enabling = params.action === 'enable';
|
||||
|
||||
// Validation
|
||||
if (!options.device && !options.application) {
|
||||
throw new ExpectedError(
|
||||
'At least one device or application must be specified',
|
||||
);
|
||||
}
|
||||
|
||||
if (options.duration != null && !enabling) {
|
||||
throw new ExpectedError(
|
||||
'--duration option is only applicable when enabling support',
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate expiry ts
|
||||
const durationDefault = '24h';
|
||||
const duration = options.duration || durationDefault;
|
||||
const expiryTs = Date.now() + this.parseDuration(duration);
|
||||
|
||||
const deviceUuids = options.device?.split(',') || [];
|
||||
const appNames = options.application?.split(',') || [];
|
||||
|
||||
const enablingMessage = 'Enabling support access for';
|
||||
const disablingMessage = 'Disabling support access for';
|
||||
|
||||
// Process devices
|
||||
for (const deviceUuid of deviceUuids) {
|
||||
if (enabling) {
|
||||
ux.action.start(`${enablingMessage} device ${deviceUuid}`);
|
||||
await balena.models.device.grantSupportAccess(deviceUuid, expiryTs);
|
||||
} else if (params.action === 'disable') {
|
||||
ux.action.start(`${disablingMessage} device ${deviceUuid}`);
|
||||
await balena.models.device.revokeSupportAccess(deviceUuid);
|
||||
}
|
||||
ux.action.stop();
|
||||
}
|
||||
|
||||
// Process applications
|
||||
for (const appName of appNames) {
|
||||
if (enabling) {
|
||||
ux.action.start(`${enablingMessage} application ${appName}`);
|
||||
await balena.models.application.grantSupportAccess(appName, expiryTs);
|
||||
} else if (params.action === 'disable') {
|
||||
ux.action.start(`${disablingMessage} application ${appName}`);
|
||||
await balena.models.application.revokeSupportAccess(appName);
|
||||
}
|
||||
ux.action.stop();
|
||||
}
|
||||
|
||||
if (enabling) {
|
||||
console.log(
|
||||
`Access has been granted for ${duration}, expiring ${new Date(
|
||||
expiryTs,
|
||||
).toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
parseDuration(duration: string): number {
|
||||
const parseErrorMsg =
|
||||
'Duration must be specified as number followed by h or d, e.g. 24h, 1d';
|
||||
const unit = duration.slice(duration.length - 1);
|
||||
const amount = Number(duration.substring(0, duration.length - 1));
|
||||
|
||||
if (isNaN(amount)) {
|
||||
throw new ExpectedError(parseErrorMsg);
|
||||
}
|
||||
|
||||
let durationMs;
|
||||
if (['h', 'H'].includes(unit)) {
|
||||
durationMs = amount * 60 * 60 * 1000;
|
||||
} else if (['d', 'D'].includes(unit)) {
|
||||
durationMs = amount * 24 * 60 * 60 * 1000;
|
||||
} else {
|
||||
throw new ExpectedError(parseErrorMsg);
|
||||
}
|
||||
|
||||
return durationMs;
|
||||
}
|
||||
}
|
@ -110,9 +110,7 @@ export default class TagSetCmd extends Command {
|
||||
throw new ExpectedError(TagSetCmd.missingResourceMessage);
|
||||
}
|
||||
|
||||
if (params.value == null) {
|
||||
params.value = '';
|
||||
}
|
||||
params.value ??= '';
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application.tags.set(
|
@ -28,7 +28,6 @@ import { getOnlineTargetUuid } from '../utils/patterns';
|
||||
import * as _ from 'lodash';
|
||||
import { tunnelConnectionToDevice } from '../utils/tunnel';
|
||||
import { createServer, Server, Socket } from 'net';
|
||||
import { tryAsInteger } from '../utils/validation';
|
||||
import { IArg } from '@oclif/parser/lib/args';
|
||||
|
||||
interface FlagsDef {
|
||||
@ -78,7 +77,6 @@ export default class TunnelCmd extends Command {
|
||||
{
|
||||
name: 'deviceOrApplication',
|
||||
description: 'device uuid or application name/id',
|
||||
parse: (x) => tryAsInteger(x),
|
||||
required: true,
|
||||
},
|
||||
];
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user