mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
121 Commits
v12.19.0
...
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 |
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',
|
||||
};
|
@ -1,3 +1,605 @@
|
||||
- commits:
|
||||
- subject: 'scan: Print production devices'' info on scan'
|
||||
hash: 7d5a64f59a47c3a051fb2cbe9e45a71029cca694
|
||||
body: ''
|
||||
footer:
|
||||
Resolves: '#1713'
|
||||
resolves: '#1713'
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
|
||||
signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
|
||||
author: Marios Balamatsias
|
||||
nested: []
|
||||
version: 12.29.0
|
||||
date: 2020-12-01T13:33:03.012Z
|
||||
- commits:
|
||||
- subject: Add ability to disable analytics for performance testing
|
||||
hash: 950d173d276f97cb3fcfd4bb9b578b5888572a69
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Connects-to: '#1708'
|
||||
connects-to: '#1708'
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.28.3
|
||||
date: 2020-11-26T12:55:50.969Z
|
||||
- commits:
|
||||
- subject: 'docs: Add references to the masterclasses in the CLI help and README'
|
||||
hash: 2bf59530c4aebdd302814e9f59d41c7b9d2672c3
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: >-
|
||||
Fix debug message logic (don't suggest --debug if it is already being
|
||||
used)
|
||||
hash: a4fd7d6118a04e8e9f0e718a765b508fb11209e6
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: Fix unhandled promise rejection when ~/.balena is not accessible
|
||||
hash: 65f053dd6e2d6e212b90e905be7af5d13772c7e6
|
||||
body: ''
|
||||
footer:
|
||||
Resolves: '#2096'
|
||||
resolves: '#2096'
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.28.2
|
||||
date: 2020-11-20T12:07:27.250Z
|
||||
- commits:
|
||||
- subject: 'scan: Prevent spinner animation output to stdout when --json is used'
|
||||
hash: 2f9212d622f7affe4391e0d6bac1a06e859b7488
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.28.1
|
||||
date: 2020-11-20T00:26:37.290Z
|
||||
- commits:
|
||||
- subject: 'push: Reduce memory usage when filtering files with dockerignore'
|
||||
hash: 4768f763856aa9f761988477f97ec872d226b004
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: 'Livepush: Refactor dockerignore filtering and add test cases'
|
||||
hash: d6b324927481ce03217c15509db2e046d74cb208
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: 'Livepush: Ignore paths set in .dockerignore files'
|
||||
hash: 02a54667469982ba676da5e0b8a0f0f379b320e5
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
Signed-off-by: Josh Bowling <josh@balena.io>
|
||||
signed-off-by: Josh Bowling <josh@balena.io>
|
||||
author: Josh Bowling
|
||||
nested: []
|
||||
version: 12.28.0
|
||||
date: 2020-11-19T17:07:59.865Z
|
||||
- commits:
|
||||
- subject: 'Test code optimization: avoid running ~70 test cases twice'
|
||||
hash: 6a556131995eebc318f5a02831a1cc1e2fb03b36
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: 'docs: Add note about macOS Big Sur notarization workaround'
|
||||
hash: 893a39e8918756db8dd4cdd5135f430a405a409e
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.27.4
|
||||
date: 2020-11-15T23:42:22.338Z
|
||||
- commits:
|
||||
- subject: Avoid reporting balenarc parsing errors
|
||||
hash: 1b0c14feab4e3c12d459d26539f65895519f89cf
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Connects-to: '#1100'
|
||||
connects-to: '#1100'
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.27.3
|
||||
date: 2020-11-11T16:54:11.888Z
|
||||
- commits:
|
||||
- subject: Modify `os download` help to mention dev images
|
||||
hash: 7da9a800ccd2af0fbb051f1d95707e5d8623e227
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Signed-off-by: Thomas Manning <thomasm@balena.io>
|
||||
signed-off-by: Thomas Manning <thomasm@balena.io>
|
||||
author: Thomas Manning
|
||||
nested: []
|
||||
version: 12.27.2
|
||||
date: 2020-11-09T12:08:41.495Z
|
||||
- commits:
|
||||
- subject: Improve application-identifier disambiguation
|
||||
hash: 46249e319ba3ee8aa1b951f5241dafa625175045
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Resolves: '#2077'
|
||||
resolves: '#2077'
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.27.1
|
||||
date: 2020-11-06T09:01:07.764Z
|
||||
- commits:
|
||||
- subject: Add command app purge
|
||||
hash: 1e18096873bf35c016a5812f91c0bf4e8ce743ba
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.27.0
|
||||
date: 2020-11-05T16:11:52.151Z
|
||||
- commits:
|
||||
- subject: >-
|
||||
config generate + openBalena v3: Fix "Cannot read property '__id' of
|
||||
undefined"
|
||||
hash: 6b4c28a0268c61413633af7351cc3f35b346d123
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.26.2
|
||||
date: 2020-11-05T13:29:37.895Z
|
||||
- commits:
|
||||
- subject: 'devices: Fix "TypeError: Cannot read property ''slug'' of undefined"'
|
||||
hash: 9d177609f5adfa357363ba5356f51027a388f635
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.26.1
|
||||
date: 2020-10-31T00:34:10.685Z
|
||||
- commits:
|
||||
- subject: Add command device purge
|
||||
hash: 47fcffe36813dfdbe59986ed6ae0e4a1a441ea63
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
Resolves: '#1547'
|
||||
resolves: '#1547'
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.26.0
|
||||
date: 2020-10-29T10:06:17.048Z
|
||||
- commits:
|
||||
- subject: 'ssh: Fix "Found more than one container with a service name <name>"'
|
||||
hash: 7f000ee8c338c88af4a41dee1a2fb924c2c2ee00
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.25.6
|
||||
date: 2020-10-28T01:11:21.452Z
|
||||
- commits:
|
||||
- subject: Remove need for hardcoded list of command ids
|
||||
hash: c48564e85a618af64c5f63722f0039f6e75862ba
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.25.5
|
||||
date: 2020-10-27T09:39:31.007Z
|
||||
- commits:
|
||||
- subject: Update Contributing document re commit messages / versionbot / changelog
|
||||
hash: 077e25ebc4e1b3f1cf5aefefcc8601f9dbe38d1f
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: 'config generate: Fix "Application is ambiguous" when app slug is used'
|
||||
hash: 709f009f9b0014acc087f154bca2f5f3ac7dec71
|
||||
body: ''
|
||||
footer:
|
||||
Connects-to: '#1893'
|
||||
connects-to: '#1893'
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: 'config generate: Fix device type compatibility check'
|
||||
hash: 116ab1fbc16be7ebc0e66192779a4f44d248d502
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.25.4
|
||||
date: 2020-10-25T17:29:17.581Z
|
||||
- commits:
|
||||
- subject: 'build/deploy: Add more test cases (--buildArg option)'
|
||||
hash: 6b208ec2abde887ffd11cfdfb624382ea7bfc049
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: Fix typing (don't assume that 'docker-toolbelt' uses Bluebird promises)
|
||||
hash: 099d755900ff9e3d994b517225da872025c3f445
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: 'build/deploy: Fix --buildArg option with docker-compose.yml projects'
|
||||
hash: 3199f15662373ca53fe1c7541259b31799e42315
|
||||
body: ''
|
||||
footer:
|
||||
Resolves: '#1053'
|
||||
resolves: '#1053'
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: 'build/deploy: Fix image size notice at end of build'
|
||||
hash: 4c8dc29946067aeaf46789058ecb310d0d862750
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: Convert more code to Typescript (compose.js)
|
||||
hash: 2b22fb89f1a2b25c532a5ec278c800e83cdcfeac
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.25.3
|
||||
date: 2020-10-21T13:37:13.594Z
|
||||
- commits:
|
||||
- subject: Revert styling of "balena CLI" as "balenaCLI"
|
||||
hash: 478b5dd363288e2556e26b86d91d08e923788bae
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: >-
|
||||
Add help note regarding the version of Node.js printed by `balena
|
||||
version -a`
|
||||
hash: 0708608c7eb710c8cd7749845384e897ba39c741
|
||||
body: ''
|
||||
footer:
|
||||
Connects-to: '#2068'
|
||||
connects-to: '#2068'
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: >-
|
||||
preload: Fix parsing of `--add-certificate` option, amend help for
|
||||
`--app`
|
||||
hash: c245dc70c244f82ee20f8c50110d302a0e86824d
|
||||
body: ''
|
||||
footer:
|
||||
Connects-to: '#2063'
|
||||
connects-to: '#2063'
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.25.2
|
||||
date: 2020-10-21T11:24:21.642Z
|
||||
- commits:
|
||||
- subject: Treat authorization errors as expected
|
||||
hash: 592efd0a2e5c0c46f4cbf0b1107b135ccab55211
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Resolves: '#2035'
|
||||
resolves: '#2035'
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.25.1
|
||||
date: 2020-10-13T08:21:31.203Z
|
||||
- commits:
|
||||
- subject: Refactor initialization code (delete app-oclif.ts and app-common.ts)
|
||||
hash: d0e4fa0e59b9c6a4c3f414361c089c68f1dfd872
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: Support BALENARC_NO_SENTRY env var to disable Sentry.io error reporting
|
||||
hash: cf376316bc4863b98223bac9c81697c2245341ae
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: Update Sentry package (may fix "Maximum call stack size exceeded")
|
||||
hash: 8f0f3bda294acda75be15d630967b526581a3c1f
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.25.0
|
||||
date: 2020-10-11T00:06:46.011Z
|
||||
- commits:
|
||||
- subject: 'login: sign up at the configured balena instance'
|
||||
hash: af70f16a9b8de7150bcdd8d76a89f72c59614526
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Signed-off-by: Matthew McGinn <matthew@balena.io>
|
||||
signed-off-by: Matthew McGinn <matthew@balena.io>
|
||||
author: Matthew McGinn
|
||||
nested: []
|
||||
version: 12.24.1
|
||||
date: 2020-10-07T13:28:42.250Z
|
||||
- commits:
|
||||
- subject: 'scan: Add ''--json'' option to help with scripting'
|
||||
hash: 8a506bc4c01e2082073b7858ab79874e707cf59d
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.24.0
|
||||
date: 2020-10-06T17:09:31.036Z
|
||||
- commits:
|
||||
- subject: Update CONTRIBUTING.md re balena-dev workflow
|
||||
hash: 89782218666af4db1297b8672560913d3de8fd8c
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: Add bin/balena* scripts to linter paths
|
||||
hash: 2974c203b5f4a1339e0feea8bf127cecbe9f13c3
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: Workaround balena-dev/oclif compatibility issues
|
||||
hash: c85acbd90bbb5de3dae13638555365b7a1cb472c
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.23.4
|
||||
date: 2020-10-05T21:13:25.405Z
|
||||
- commits:
|
||||
- subject: Rename actions-oclif/ to commands/
|
||||
hash: eddbdfe0dcea8df801d11d398e72f34c8354f7e1
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.23.3
|
||||
date: 2020-10-02T11:45:45.519Z
|
||||
- commits:
|
||||
- subject: 'push: Fix accidental rename of `-e` (emulated) option'
|
||||
hash: 6133bb209687d5fe208ee1c31d19a435d9c077c3
|
||||
body: |
|
||||
Accidentally renamed during oclif conversion in CLI v12.9.7.
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.23.2
|
||||
date: 2020-10-02T09:00:21.328Z
|
||||
- commits:
|
||||
- subject: Update the CONTRIBUTING.md document
|
||||
hash: 48076464daa4d3aa6e86db6fe133d64ed50cf932
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.23.1
|
||||
date: 2020-09-28T14:22:42.550Z
|
||||
- commits:
|
||||
- subject: Add new command `support`
|
||||
hash: 0a8b3ce4e4a5b333f8aa6fd5f51e98444a42d966
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
Resolves: '#766 #1546'
|
||||
resolves: '#766 #1546'
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.23.0
|
||||
date: 2020-09-25T14:24:11.954Z
|
||||
- commits:
|
||||
- subject: 'deploy: Fix unexpected exit with "Everything is up to date"'
|
||||
hash: 889fafcffce017373d7b6896ce9fd0a18b6b55f2
|
||||
body: ''
|
||||
footer:
|
||||
Resolves: '#2040'
|
||||
resolves: '#2040'
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.22.2
|
||||
date: 2020-09-19T23:41:31.910Z
|
||||
- commits:
|
||||
- subject: Style "balena CLI" as "balenaCLI" and "balena cloud" as "balenaCloud"
|
||||
hash: b1897a512d42ce2fe3fec859cd375524637f8fce
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
- subject: Reorganize and simplify installation instructions
|
||||
hash: f98c25eaee2f15f5e13f26f05361829fb1e5b5dd
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.22.1
|
||||
date: 2020-09-19T00:24:21.430Z
|
||||
- commits:
|
||||
- subject: Add new command `device restart`
|
||||
hash: abdaf0043fea01d21d29116bb128d6763b01d576
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
Resolves: '#1542'
|
||||
resolves: '#1542'
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.22.0
|
||||
date: 2020-09-18T10:42:08.067Z
|
||||
- commits:
|
||||
- subject: 'scan: Fix "CLI could not be loaded" with the standalone zip installer'
|
||||
hash: 799e0f9dea608be28836083f894f93a3a9626c3e
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Paulo Castro
|
||||
nested: []
|
||||
version: 12.21.1
|
||||
date: 2020-09-16T23:41:01.211Z
|
||||
- commits:
|
||||
- subject: Add new command `app rename`
|
||||
hash: 3706db2436371cd797501eaf26132003b4902ad0
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
Resolves: '#1567'
|
||||
resolves: '#1567'
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.21.0
|
||||
date: 2020-09-16T14:40:41.774Z
|
||||
- commits:
|
||||
- subject: Minor fix to device rm
|
||||
hash: b180eb7b73a7d340d0cde534425d06f5f06b7396
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
- subject: Update registry secrets example URL
|
||||
hash: 9805854eab2ae7bb6dbc0e545f9f06a9dc2be714
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
- subject: 'Improve command suggestions, add topic help'
|
||||
hash: 00c956394d44cd0270eda93fba4498ea2e4f9881
|
||||
body: ''
|
||||
footer:
|
||||
Change-type: minor
|
||||
change-type: minor
|
||||
Resolves: '#2021'
|
||||
resolves: '#2021'
|
||||
Signed-off-by: Scott Lowe <scott@balena.io>
|
||||
signed-off-by: Scott Lowe <scott@balena.io>
|
||||
author: Scott Lowe
|
||||
nested: []
|
||||
version: 12.20.0
|
||||
date: 2020-09-10T17:55:01.446Z
|
||||
- commits:
|
||||
- subject: Fix numerical id support in device rm
|
||||
hash: f9224b05af886944fb4ec5f5de2a0dfda1b712e8
|
||||
|
152
CHANGELOG.md
152
CHANGELOG.md
@ -4,6 +4,158 @@ 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]
|
||||
|
212
CONTRIBUTING.md
212
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,72 +112,70 @@ 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). You can improve this by installing the npm merge driver with:
|
||||
```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)
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Whether or not there is a merge error, the following commands are the recommended way of updating
|
||||
and committing the `npm-shrinkwrap.json` file:
|
||||
|
||||
```bash
|
||||
$ npm install # fetch the latest modules update the npm-shrinkwrap.json file
|
||||
$ npm dedupe # deduplicate dependencies from the npm-shrinkwrap.json file
|
||||
$ npm install # re-add optional dependencies for other platforms that may have been removed by dedupe
|
||||
$ git add npm-shrinkwrap.json # add it for committing (solve merge errors)
|
||||
```
|
||||
|
||||
## TypeScript and oclif
|
||||
|
||||
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
|
||||
|
||||
@ -138,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';
|
||||
|
@ -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: ['help', '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-oclif/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-oclif/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'),
|
||||
]);
|
||||
|
@ -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',
|
||||
);
|
||||
}
|
||||
|
721
doc/cli.markdown
721
doc/cli.markdown
@ -1,24 +1,23 @@
|
||||
# Balena CLI Documentation
|
||||
# balena CLI Documentation
|
||||
|
||||
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:
|
||||
@ -36,17 +35,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:
|
||||
@ -55,7 +54,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):
|
||||
@ -81,19 +80,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
|
||||
@ -121,21 +127,23 @@ address like `192.168.1.2`.
|
||||
|
||||
## 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.
|
||||
|
||||
@ -156,8 +164,10 @@ Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
- [apps](#apps)
|
||||
- [app <name>](#app-name)
|
||||
- [app create <name>](#app-create-name)
|
||||
- [app rm <name>](#app-rm-name)
|
||||
- [app purge <name>](#app-purge-name)
|
||||
- [app rename <name> [newname]](#app-rename-name-newname)
|
||||
- [app restart <name>](#app-restart-name)
|
||||
- [app rm <name>](#app-rm-name)
|
||||
|
||||
- Authentication
|
||||
|
||||
@ -167,19 +177,21 @@ Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
- Device
|
||||
|
||||
- [devices](#devices)
|
||||
- [devices supported](#devices-supported)
|
||||
- [device <uuid>](#device-uuid)
|
||||
- [device identify <uuid>](#device-identify-uuid)
|
||||
- [device init](#device-init)
|
||||
- [device <uuid>](#device-uuid)
|
||||
- [device move <uuid(s)>](#device-move-uuid-s)
|
||||
- [device os-update <uuid>](#device-os-update-uuid)
|
||||
- [device public-url <uuid>](#device-public-url-uuid)
|
||||
- [device purge <uuid>](#device-purge-uuid)
|
||||
- [device reboot <uuid>](#device-reboot-uuid)
|
||||
- [device register <application>](#device-register-application)
|
||||
- [device rename <uuid> [newname]](#device-rename-uuid-newname)
|
||||
- [device restart <uuid>](#device-restart-uuid)
|
||||
- [device rm <uuid(s)>](#device-rm-uuid-s)
|
||||
- [device shutdown <uuid>](#device-shutdown-uuid)
|
||||
- [devices](#devices)
|
||||
- [devices supported](#devices-supported)
|
||||
- [device os-update <uuid>](#device-os-update-uuid)
|
||||
- [device public-url <uuid>](#device-public-url-uuid)
|
||||
|
||||
- Environment Variables
|
||||
|
||||
@ -267,6 +279,10 @@ Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
- [util available-drives](#util-available-drives)
|
||||
|
||||
- Support
|
||||
|
||||
- [support <action>](#support-action)
|
||||
|
||||
# API keys
|
||||
|
||||
## api-key generate <name>
|
||||
@ -352,6 +368,63 @@ application name
|
||||
|
||||
application device type (Check available types with `balena devices supported`)
|
||||
|
||||
## app purge <name>
|
||||
|
||||
Purge data from all devices belonging to an application.
|
||||
This will clear the application's /data directory.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app purge MyApp
|
||||
|
||||
### Arguments
|
||||
|
||||
#### NAME
|
||||
|
||||
application name or numeric ID
|
||||
|
||||
### Options
|
||||
|
||||
## app rename <name> [newName]
|
||||
|
||||
Rename an application.
|
||||
|
||||
Note, if the `newName` parameter is omitted, it will be
|
||||
prompted for interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app rename OldName
|
||||
$ balena app rename OldName NewName
|
||||
|
||||
### Arguments
|
||||
|
||||
#### NAME
|
||||
|
||||
application name or numeric ID
|
||||
|
||||
#### NEWNAME
|
||||
|
||||
the new name for the application
|
||||
|
||||
### Options
|
||||
|
||||
## app restart <name>
|
||||
|
||||
Restart all devices belonging to an application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app restart MyApp
|
||||
|
||||
### Arguments
|
||||
|
||||
#### NAME
|
||||
|
||||
application name or numeric ID
|
||||
|
||||
### Options
|
||||
|
||||
## app rm <name>
|
||||
|
||||
Permanently remove a balena application.
|
||||
@ -375,22 +448,6 @@ application name or numeric ID
|
||||
|
||||
answer "yes" to all questions (non interactive use)
|
||||
|
||||
## app restart <name>
|
||||
|
||||
Restart all devices that belongs to a certain application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app restart MyApp
|
||||
|
||||
### Arguments
|
||||
|
||||
#### NAME
|
||||
|
||||
application name or numeric ID
|
||||
|
||||
### Options
|
||||
|
||||
# Authentication
|
||||
|
||||
## login
|
||||
@ -468,6 +525,90 @@ Examples:
|
||||
|
||||
# Device
|
||||
|
||||
## devices
|
||||
|
||||
list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the `--application` option.
|
||||
|
||||
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, empty strings and null values.
|
||||
The 'jq' utility may be helpful for querying JSON fields in shell scripts
|
||||
(https://stedolan.github.io/jq/manual/).
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena devices
|
||||
$ balena devices --application MyApp
|
||||
$ balena devices --app MyApp
|
||||
$ balena devices -a MyApp
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
application name
|
||||
|
||||
#### --app APP
|
||||
|
||||
same as '--application'
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
## devices supported
|
||||
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
The --verbose option adds extra columns/fields to the output, including the
|
||||
"STATE" column whose values are one of 'new', 'released' or 'discontinued'.
|
||||
However, 'discontinued' device types are only listed if the '--discontinued'
|
||||
option is used.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings (for example, the ALIASES column contains a
|
||||
list of zero or more values). The 'jq' utility may be helpful in shell scripts
|
||||
(https://stedolan.github.io/jq/manual/).
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena devices supported
|
||||
$ balena devices supported --verbose
|
||||
$ balena devices supported -vj
|
||||
|
||||
### Options
|
||||
|
||||
#### --discontinued
|
||||
|
||||
include "discontinued" device types
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
#### -v, --verbose
|
||||
|
||||
add extra columns in the tabular output (ALIASES, ARCH, STATE)
|
||||
|
||||
## device <uuid>
|
||||
|
||||
Show information about a single device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device 7cf02a6
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
the device uuid
|
||||
|
||||
### Options
|
||||
|
||||
## device identify <uuid>
|
||||
|
||||
Identify a device by making the ACT LED blink (Raspberry Pi).
|
||||
@ -533,22 +674,6 @@ Check `balena util available-drives` for available options.
|
||||
|
||||
path to the config JSON file, see `balena os build-config`
|
||||
|
||||
## device <uuid>
|
||||
|
||||
Show information about a single device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device 7cf02a6
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
the device uuid
|
||||
|
||||
### Options
|
||||
|
||||
## device move <uuid(s)>
|
||||
|
||||
Move one or more devices to another application.
|
||||
@ -578,182 +703,6 @@ application name
|
||||
|
||||
same as '--application'
|
||||
|
||||
## device reboot <uuid>
|
||||
|
||||
Remotely reboot a device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device reboot 23c73a1
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
the uuid of the device to reboot
|
||||
|
||||
### Options
|
||||
|
||||
#### -f, --force
|
||||
|
||||
force action if the update lock is set
|
||||
|
||||
## device register <application>
|
||||
|
||||
Register a device to an application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device register MyApp
|
||||
$ balena device register MyApp --uuid <uuid>
|
||||
|
||||
### Arguments
|
||||
|
||||
#### APPLICATION
|
||||
|
||||
the name or id of application to register device with
|
||||
|
||||
### Options
|
||||
|
||||
#### -u, --uuid UUID
|
||||
|
||||
custom uuid
|
||||
|
||||
## device rename <uuid> [newName]
|
||||
|
||||
Rename a device.
|
||||
|
||||
Note, if the name is omitted, it will be prompted for interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rename 7cf02a6
|
||||
$ balena device rename 7cf02a6 MyPi
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
the uuid of the device to rename
|
||||
|
||||
#### NEWNAME
|
||||
|
||||
the new name for the device
|
||||
|
||||
### Options
|
||||
|
||||
## device rm <uuid(s)>
|
||||
|
||||
Remove one or more devices from balena.
|
||||
|
||||
Note this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rm 7cf02a6
|
||||
$ balena device rm 7cf02a6,dc39e52
|
||||
$ balena device rm 7cf02a6 --yes
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
comma-separated list (no blank spaces) of device UUIDs to be removed
|
||||
|
||||
### Options
|
||||
|
||||
#### -y, --yes
|
||||
|
||||
answer "yes" to all questions (non interactive use)
|
||||
|
||||
## device shutdown <uuid>
|
||||
|
||||
Remotely shutdown a device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device shutdown 23c73a1
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
the uuid of the device to shutdown
|
||||
|
||||
### Options
|
||||
|
||||
#### -f, --force
|
||||
|
||||
force action if the update lock is set
|
||||
|
||||
## devices
|
||||
|
||||
list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the `--application` option.
|
||||
|
||||
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/).
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena devices
|
||||
$ balena devices --application MyApp
|
||||
$ balena devices --app MyApp
|
||||
$ balena devices -a MyApp
|
||||
|
||||
### Options
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
application name
|
||||
|
||||
#### --app APP
|
||||
|
||||
same as '--application'
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
## devices supported
|
||||
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
The --verbose option adds extra columns/fields to the output, including the
|
||||
"STATE" column whose values are one of 'new', 'released' or 'discontinued'.
|
||||
However, 'discontinued' device types are only listed if the '--discontinued'
|
||||
option is used.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings (for example, the ALIASES column contains a
|
||||
list of zero or more values). The 'jq' utility may be helpful in shell scripts
|
||||
(https://stedolan.github.io/jq/manual/).
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena devices supported
|
||||
$ balena devices supported --verbose
|
||||
$ balena devices supported -vj
|
||||
|
||||
### Options
|
||||
|
||||
#### --discontinued
|
||||
|
||||
include "discontinued" device types
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
#### -v, --verbose
|
||||
|
||||
add extra columns in the tabular output (ALIASES, ARCH, STATE)
|
||||
|
||||
## device os-update <uuid>
|
||||
|
||||
Start a Host OS update for a device.
|
||||
@ -824,6 +773,166 @@ disable the public URL
|
||||
|
||||
determine if public URL is enabled
|
||||
|
||||
## device purge <uuid>
|
||||
|
||||
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).
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device purge 23c73a1
|
||||
$ balena device purge 55d43b3,23c73a1
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
comma-separated list (no blank spaces) of device UUIDs
|
||||
|
||||
### Options
|
||||
|
||||
## device reboot <uuid>
|
||||
|
||||
Remotely reboot a device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device reboot 23c73a1
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
the uuid of the device to reboot
|
||||
|
||||
### Options
|
||||
|
||||
#### -f, --force
|
||||
|
||||
force action if the update lock is set
|
||||
|
||||
## device register <application>
|
||||
|
||||
Register a device to an application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device register MyApp
|
||||
$ balena device register MyApp --uuid <uuid>
|
||||
|
||||
### Arguments
|
||||
|
||||
#### APPLICATION
|
||||
|
||||
the name or id of application to register device with
|
||||
|
||||
### Options
|
||||
|
||||
#### -u, --uuid UUID
|
||||
|
||||
custom uuid
|
||||
|
||||
## device rename <uuid> [newName]
|
||||
|
||||
Rename a device.
|
||||
|
||||
Note, if the name is omitted, it will be prompted for interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rename 7cf02a6
|
||||
$ balena device rename 7cf02a6 MyPi
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
the uuid of the device to rename
|
||||
|
||||
#### NEWNAME
|
||||
|
||||
the new name for the device
|
||||
|
||||
### Options
|
||||
|
||||
## device restart <uuid>
|
||||
|
||||
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`.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device restart 23c73a1
|
||||
$ balena device restart 55d43b3,23c73a1
|
||||
$ balena device restart 23c73a1 --service myService
|
||||
$ balena device restart 23c73a1 -s myService1,myService2
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
comma-separated list (no blank spaces) of device UUIDs to restart
|
||||
|
||||
### Options
|
||||
|
||||
#### -s, --service SERVICE
|
||||
|
||||
comma-separated list (no blank spaces) of service names to restart
|
||||
|
||||
## device rm <uuid(s)>
|
||||
|
||||
Remove one or more devices from balena.
|
||||
|
||||
Note this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rm 7cf02a6
|
||||
$ balena device rm 7cf02a6,dc39e52
|
||||
$ balena device rm 7cf02a6 --yes
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
comma-separated list (no blank spaces) of device UUIDs to be removed
|
||||
|
||||
### Options
|
||||
|
||||
#### -y, --yes
|
||||
|
||||
answer "yes" to all questions (non interactive use)
|
||||
|
||||
## device shutdown <uuid>
|
||||
|
||||
Remotely shutdown a device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device shutdown 23c73a1
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUID
|
||||
|
||||
the uuid of the device to shutdown
|
||||
|
||||
### Options
|
||||
|
||||
#### -f, --force
|
||||
|
||||
force action if the update lock is set
|
||||
|
||||
# Environment Variables
|
||||
|
||||
## envs
|
||||
@ -1236,7 +1345,11 @@ show additional commands
|
||||
|
||||
## version
|
||||
|
||||
Display version information for the balena CLI and/or Node.js.
|
||||
Display version information for the balena CLI and/or Node.js. Note that the
|
||||
balena CLI executable installers for Windows and macOS, and the standalone
|
||||
zip packages, ship with a built-in copy of Node.js. In this case, the
|
||||
reported version of Node.js regards this built-in copy, rather than any
|
||||
other `node` engine that may also be available on the command prompt.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents
|
||||
@ -1293,15 +1406,30 @@ balenaCloud ID for the SSH key
|
||||
|
||||
## key add <name> [path]
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
### Arguments
|
||||
|
||||
@ -1396,6 +1524,11 @@ Only show system logs. This can be used in combination with --service.
|
||||
|
||||
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.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena scan
|
||||
@ -1412,6 +1545,10 @@ display full info
|
||||
|
||||
scan timeout in seconds
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
## ssh <applicationOrDevice> [service]
|
||||
|
||||
Start a shell on a local or remote device. If a service name is not provided,
|
||||
@ -1584,11 +1721,15 @@ versions for the given device type are pre-release).
|
||||
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.
|
||||
|
||||
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
|
||||
@ -1987,7 +2128,7 @@ the image file path
|
||||
|
||||
#### -a, --app APP
|
||||
|
||||
name, slug or numeric ID of the application to preload
|
||||
name of the application to preload
|
||||
|
||||
#### -c, --commit COMMIT
|
||||
|
||||
@ -2042,8 +2183,7 @@ Docker host TLS key file
|
||||
|
||||
## push <applicationOrDevice>
|
||||
|
||||
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
|
||||
@ -2082,7 +2222,7 @@ Sample registry-secrets YAML file:
|
||||
password: '{escaped contents of the GCR keyfile.json file}'
|
||||
```
|
||||
For a sample project using registry secrets with the Google Container Registry,
|
||||
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
||||
check: https://github.com/balena-io-examples/sample-gcr-registry-secrets
|
||||
|
||||
If the --registry-secrets option is not specified, and a secrets.yml or
|
||||
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
||||
@ -2114,7 +2254,7 @@ compatibility with the standard docker-compose tool, while still allowing a
|
||||
root .dockerignore file (at the overall project root) to filter files and
|
||||
folders that are outside service subdirectories.
|
||||
|
||||
Balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||
option if compatibility is required. This option is mutually exclusive with
|
||||
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||
@ -2171,9 +2311,10 @@ application name, or device address (for local pushes)
|
||||
Source directory to be sent to balenaCloud or balenaOS device
|
||||
(default: current working dir)
|
||||
|
||||
#### -f, --emulated
|
||||
#### -e, --emulated
|
||||
|
||||
Force an emulated build to occur on the remote builder
|
||||
Don't use native ARM servers; force QEMU ARM emulation on Intel x86-64
|
||||
servers during the image build (balenaCloud).
|
||||
|
||||
#### --dockerfile DOCKERFILE
|
||||
|
||||
@ -2265,7 +2406,7 @@ required until your project can be adapted.
|
||||
|
||||
## settings
|
||||
|
||||
Use this command to display current balena CLI settings.
|
||||
Use this command to display the current balena CLI settings.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -2358,7 +2499,7 @@ Sample registry-secrets YAML file:
|
||||
password: '{escaped contents of the GCR keyfile.json file}'
|
||||
```
|
||||
For a sample project using registry secrets with the Google Container Registry,
|
||||
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
||||
check: https://github.com/balena-io-examples/sample-gcr-registry-secrets
|
||||
|
||||
If the --registry-secrets option is not specified, and a secrets.yml or
|
||||
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
||||
@ -2390,7 +2531,7 @@ compatibility with the standard docker-compose tool, while still allowing a
|
||||
root .dockerignore file (at the overall project root) to filter files and
|
||||
folders that are outside service subdirectories.
|
||||
|
||||
Balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||
option if compatibility is required. This option is mutually exclusive with
|
||||
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||
@ -2447,7 +2588,7 @@ name of the target balena application this build is for
|
||||
|
||||
#### -e, --emulated
|
||||
|
||||
Run an emulated build using Qemu
|
||||
Use QEMU for ARM architecture emulation during the image build
|
||||
|
||||
#### --dockerfile DOCKERFILE
|
||||
|
||||
@ -2580,7 +2721,7 @@ Sample registry-secrets YAML file:
|
||||
password: '{escaped contents of the GCR keyfile.json file}'
|
||||
```
|
||||
For a sample project using registry secrets with the Google Container Registry,
|
||||
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
||||
check: https://github.com/balena-io-examples/sample-gcr-registry-secrets
|
||||
|
||||
If the --registry-secrets option is not specified, and a secrets.yml or
|
||||
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
||||
@ -2612,7 +2753,7 @@ compatibility with the standard docker-compose tool, while still allowing a
|
||||
root .dockerignore file (at the overall project root) to filter files and
|
||||
folders that are outside service subdirectories.
|
||||
|
||||
Balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||
option if compatibility is required. This option is mutually exclusive with
|
||||
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||
@ -2670,7 +2811,7 @@ don't upload build logs to the dashboard with image (if building)
|
||||
|
||||
#### -e, --emulated
|
||||
|
||||
Run an emulated build using Qemu
|
||||
Use QEMU for ARM architecture emulation during the image build
|
||||
|
||||
#### --dockerfile DOCKERFILE
|
||||
|
||||
@ -2844,3 +2985,43 @@ List available drives which are usable for writing an OS image to.
|
||||
Does not list system drives.
|
||||
|
||||
### Options
|
||||
|
||||
# Support
|
||||
|
||||
## support <action>
|
||||
|
||||
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).
|
||||
|
||||
Examples:
|
||||
|
||||
balena support enable --device ab346f,cd457a --duration 3d
|
||||
balena support enable --application app3 --duration 12h
|
||||
balena support disable -a myApp
|
||||
|
||||
### Arguments
|
||||
|
||||
#### ACTION
|
||||
|
||||
enable|disable support access
|
||||
|
||||
### Options
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
comma-separated list (no spaces) of device UUIDs
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
comma-separated list (no spaces) of application names
|
||||
|
||||
#### -t, --duration DURATION
|
||||
|
||||
length of time to enable support for, in (h)ours or (d)ays, e.g. 12h, 2d
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
190
lib/app.ts
190
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,78 +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 { preparseArgs, checkDeletedCommand } = 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();
|
||||
|
||||
// Look for commands that have been removed and if so, exit with a notice
|
||||
checkDeletedCommand(cliArgs.slice(2));
|
||||
|
||||
const args = await preparseArgs(cliArgs);
|
||||
await (await import('./app-oclif')).run(args, 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>
|
||||
|
@ -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'];
|
||||
|
@ -22,8 +22,8 @@ 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 {
|
||||
@ -219,7 +219,7 @@ ${dockerignoreHelp}
|
||||
arch: string;
|
||||
deviceType: string;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: any;
|
||||
buildOpts: BuildOpts;
|
||||
},
|
||||
) {
|
||||
const { loadProject } = await import('../utils/compose_ts');
|
||||
@ -238,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
|
@ -16,14 +16,24 @@
|
||||
*/
|
||||
|
||||
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 { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
||||
import type {
|
||||
BuiltImage,
|
||||
ComposeCliFlags,
|
||||
ComposeOpts,
|
||||
} from '../utils/compose-types';
|
||||
import type { DockerCliFlags } from '../utils/docker';
|
||||
import { composeCliFlags } from '../utils/compose_ts';
|
||||
import {
|
||||
buildProject,
|
||||
composeCliFlags,
|
||||
isBuildConfig,
|
||||
} from '../utils/compose_ts';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
import type { Application, ApplicationType, DeviceType } from 'balena-sdk';
|
||||
|
||||
@ -214,22 +224,21 @@ ${dockerignoreHelp}
|
||||
}
|
||||
|
||||
// find which services use images that already exist locally
|
||||
let servicesToSkip = await Promise.all(
|
||||
project.descriptors.map(async function (d: any) {
|
||||
let servicesToSkip: string[] = await Promise.all(
|
||||
project.descriptors.map(async function (d: ImageDescriptor) {
|
||||
// unconditionally build (or pull) if explicitly requested
|
||||
if (opts.shouldPerformBuild) {
|
||||
return d;
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
await docker
|
||||
.getImage(
|
||||
(typeof d.image === 'string' ? d.image : d.image.tag) || '',
|
||||
)
|
||||
.getImage((isBuildConfig(d.image) ? d.image.tag : d.image) || '')
|
||||
.inspect();
|
||||
|
||||
return d.serviceName;
|
||||
} catch {
|
||||
// Ignore
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
@ -243,35 +252,35 @@ ${dockerignoreHelp}
|
||||
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)',
|
||||
);
|
||||
return {};
|
||||
} 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 builtImages = await compose.buildProject(
|
||||
docker,
|
||||
logger,
|
||||
project.path,
|
||||
project.name,
|
||||
compositionToBuild,
|
||||
opts.app.arch,
|
||||
(opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
|
||||
opts.buildEmulated,
|
||||
opts.buildOpts,
|
||||
composeOpts.inlineLogs,
|
||||
composeOpts.convertEol,
|
||||
composeOpts.dockerfilePath,
|
||||
composeOpts.nogitignore,
|
||||
composeOpts.multiDockerignore,
|
||||
);
|
||||
const builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
||||
|
||||
const images = project.descriptors.map(
|
||||
const images: BuiltImage[] = project.descriptors.map(
|
||||
(d) =>
|
||||
builtImagesByService[d.serviceName] ?? {
|
||||
serviceName: d.serviceName,
|
||||
name: typeof d.image === 'string' ? d.image : d.image.tag,
|
||||
name: (isBuildConfig(d.image) ? d.image.tag : d.image) || '',
|
||||
logs: 'Build skipped; image for service already exists.',
|
||||
props: {},
|
||||
},
|
@ -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()),
|
||||
{
|
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);
|
||||
}
|
||||
}
|
@ -82,7 +82,7 @@ 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(tryAsInteger(uuid));
|
||||
} catch (err) {
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = [
|
@ -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}`);
|
||||
}
|
@ -184,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;
|
||||
@ -199,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',
|
@ -43,7 +43,7 @@ interface FlagsDef extends DockerConnectionCliFlags {
|
||||
'splash-image'?: string;
|
||||
'dont-check-arch': boolean;
|
||||
'pin-device-to-release': boolean;
|
||||
'add-certificate'?: string;
|
||||
'add-certificate'?: string[];
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ export default class PreloadCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
app: flags.string({
|
||||
description: 'name, slug or numeric ID of the application to preload',
|
||||
description: 'name of the application to preload',
|
||||
char: 'a',
|
||||
}),
|
||||
commit: flags.string({
|
||||
@ -115,6 +115,7 @@ The file name must end with '.crt' and must not be already contained in the prel
|
||||
/etc/ssl/certs folder.
|
||||
Can be repeated to add multiple certificates.\
|
||||
`,
|
||||
multiple: true,
|
||||
}),
|
||||
...dockerConnectionCliFlags,
|
||||
// Redefining --dockerPort here (defined already in dockerConnectionCliFlags)
|
||||
@ -201,14 +202,7 @@ Can be repeated to add multiple certificates.\
|
||||
);
|
||||
}
|
||||
|
||||
let certificates: string[];
|
||||
if (Array.isArray(options['add-certificate'])) {
|
||||
certificates = options['add-certificate'];
|
||||
} else if (options['add-certificate'] === undefined) {
|
||||
certificates = [];
|
||||
} else {
|
||||
certificates = [options['add-certificate']];
|
||||
}
|
||||
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"');
|
||||
@ -463,10 +457,12 @@ Would you like to disable automatic updates for this application now?\
|
||||
});
|
||||
}
|
||||
|
||||
getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) {
|
||||
return balenaSdk.models.application.get(appId, {
|
||||
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) {
|
||||
const { getApplication } = await import('../utils/sdk');
|
||||
|
||||
return (await getApplication(balenaSdk, appId, {
|
||||
$expand: this.applicationExpandOptions,
|
||||
}) as Promise<Application & { should_be_running__release: [Release?] }>;
|
||||
})) as Application & { should_be_running__release: [Release?] };
|
||||
}
|
||||
|
||||
async prepareAndPreload(
|
@ -55,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
|
||||
@ -122,8 +121,10 @@ export default class PushCmd extends Command {
|
||||
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:
|
@ -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'];
|
||||
|
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;
|
||||
}
|
||||
}
|
@ -34,7 +34,11 @@ export default class VersionCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display version information for the balena CLI and/or Node.js.
|
||||
|
||||
Display version information for the balena CLI and/or Node.js.
|
||||
Display version information for the balena CLI and/or Node.js. Note that the
|
||||
balena CLI executable installers for Windows and macOS, and the standalone
|
||||
zip packages, ship with a built-in copy of Node.js. In this case, the
|
||||
reported version of Node.js regards this built-in copy, rather than any
|
||||
other \`node\` engine that may also be available on the command prompt.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents
|
@ -148,13 +148,15 @@ const EXPECTED_ERROR_REGEXES = [
|
||||
/^BalenaDeviceNotFound/, // balena-sdk
|
||||
/^BalenaExpiredToken/, // balena-sdk
|
||||
/^BalenaInvalidDeviceType/, // balena-sdk
|
||||
/^Missing \w+$/, // Capitano,
|
||||
/Request error: Unauthorized$/, // balena-sdk
|
||||
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError
|
||||
/Missing required flag/, // oclif parser: RequiredFlagError
|
||||
/^Unexpected argument/, // oclif parser: UnexpectedArgsError
|
||||
/to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError
|
||||
/must also be provided when using/, // oclif parser (depends-on)
|
||||
/^Expected an integer/, // oclif parser (flags.integer)
|
||||
/^Flag .* expects a value/, // oclif parser
|
||||
/^Error parsing config file.*balenarc\.yml/,
|
||||
];
|
||||
|
||||
// Support unit testing of handleError
|
||||
@ -162,6 +164,18 @@ export const getSentry = async function () {
|
||||
return await import('@sentry/node');
|
||||
};
|
||||
|
||||
async function sentryCaptureException(error: Error) {
|
||||
const Sentry = await getSentry();
|
||||
Sentry.captureException(error);
|
||||
try {
|
||||
await Sentry.close(1000);
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error('Timeout reporting error to sentry.io');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleError(error: Error) {
|
||||
// Set appropriate exitCode
|
||||
process.exitCode =
|
||||
@ -189,15 +203,10 @@ export async function handleError(error: Error) {
|
||||
printErrorMessage(message.join('\n'));
|
||||
|
||||
// Report "unexpected" errors via Sentry.io
|
||||
const Sentry = await getSentry();
|
||||
Sentry.captureException(error);
|
||||
try {
|
||||
await Sentry.close(1000);
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error('Timeout reporting error to sentry.io');
|
||||
}
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
await sentryCaptureException(error);
|
||||
}
|
||||
|
||||
// Unhandled/unexpected error: ensure that the process terminates.
|
||||
// The exit error code was set above through `process.exitCode`.
|
||||
process.exit();
|
||||
@ -215,7 +224,7 @@ export const printErrorMessage = function (message: string) {
|
||||
console.error(line);
|
||||
});
|
||||
|
||||
console.error(`\n${getHelp}\n`);
|
||||
console.error(`\n${getHelp()}\n`);
|
||||
};
|
||||
|
||||
export const printExpectedErrorMessage = function (message: string) {
|
||||
|
@ -46,13 +46,19 @@ interface CachedUsername {
|
||||
* The username and command signature are also added as extra context
|
||||
* information in Sentry.io error reporting, for CLI debugging purposes
|
||||
* (mainly unexpected/unhandled exceptions -- see also `lib/errors.ts`).
|
||||
*
|
||||
* For more details on the data collected by balena generally, check this page:
|
||||
* https://www.balena.io/docs/learn/more/collected-data/
|
||||
*/
|
||||
export async function trackCommand(commandSignature: string) {
|
||||
try {
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setExtra('command', commandSignature);
|
||||
});
|
||||
let Sentry: typeof import('@sentry/node');
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry = await import('@sentry/node');
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setExtra('command', commandSignature);
|
||||
});
|
||||
}
|
||||
const settings = await import('balena-settings-client');
|
||||
const balenaUrl = settings.get('balenaUrl') as string;
|
||||
|
||||
@ -78,7 +84,7 @@ export async function trackCommand(commandSignature: string) {
|
||||
try {
|
||||
const balena = getBalenaSdk();
|
||||
const $username = await balena.auth.whoami();
|
||||
storage.set('cachedUsername', {
|
||||
await storage.set('cachedUsername', {
|
||||
token,
|
||||
username: $username,
|
||||
} as CachedUsername);
|
||||
@ -90,14 +96,19 @@ export async function trackCommand(commandSignature: string) {
|
||||
|
||||
const mixpanel = getMixpanel(balenaUrl);
|
||||
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setUser({
|
||||
id: username,
|
||||
username,
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry!.configureScope((scope) => {
|
||||
scope.setUser({
|
||||
id: username,
|
||||
username,
|
||||
});
|
||||
});
|
||||
});
|
||||
// Don't actually call mixpanel.track() while running test cases
|
||||
if (!process.env.BALENA_CLI_TEST_TYPE) {
|
||||
}
|
||||
// Don't actually call mixpanel.track() while running test cases, or if suppressed
|
||||
if (
|
||||
!process.env.BALENA_CLI_TEST_TYPE &&
|
||||
!process.env.BALENARC_NO_ANALYTICS
|
||||
) {
|
||||
await mixpanel.track(`[CLI] ${commandSignature}`, {
|
||||
distinct_id: username,
|
||||
version: packageJSON.version,
|
||||
|
51
lib/help.ts
51
lib/help.ts
@ -1,3 +1,19 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017-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 Help from '@oclif/plugin-help';
|
||||
import * as indent from 'indent-string';
|
||||
import { getChalk } from './utils/lazy';
|
||||
@ -40,6 +56,27 @@ export default class BalenaHelp extends Help {
|
||||
return;
|
||||
}
|
||||
|
||||
// If they've typed a topic (e.g. `balena os`) that isn't also a command (e.g. `balena device`)
|
||||
// then list the associated commands.
|
||||
const topicCommands = this.config.commands.filter((c) => {
|
||||
return c.id.startsWith(`${subject}:`);
|
||||
});
|
||||
if (topicCommands.length > 0) {
|
||||
console.log(`${chalk.yellow(subject)} commands include:`);
|
||||
console.log(this.formatCommands(topicCommands));
|
||||
console.log(
|
||||
`\nRun ${chalk.cyan.bold(
|
||||
'balena help -v',
|
||||
)} for a list of all available commands,`,
|
||||
);
|
||||
console.log(
|
||||
` or ${chalk.cyan.bold(
|
||||
'balena help <command>',
|
||||
)} for detailed help on a specific command.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`);
|
||||
}
|
||||
|
||||
@ -102,16 +139,10 @@ export default class BalenaHelp extends Help {
|
||||
console.log(' --help, -h');
|
||||
console.log(' --debug\n');
|
||||
|
||||
console.log(
|
||||
`For help, visit our support forums: ${chalk.grey(
|
||||
'https://forums.balena.io',
|
||||
)}`,
|
||||
);
|
||||
console.log(
|
||||
`For bug reports or feature requests, see: ${chalk.grey(
|
||||
'https://github.com/balena-io/balena-cli/issues/',
|
||||
)}\n`,
|
||||
);
|
||||
const {
|
||||
reachingOut,
|
||||
} = require('./utils/messages') as typeof import('./utils/messages');
|
||||
console.log(reachingOut);
|
||||
}
|
||||
|
||||
protected formatCommands(commands: any[]): string {
|
||||
|
@ -22,10 +22,12 @@ import type { IConfig } from '@oclif/config';
|
||||
A modified version of the command-not-found plugin logic,
|
||||
which deals with spaces separators stead of colons, and
|
||||
prints suggested commands instead of prompting interactively.
|
||||
|
||||
Also see help.ts showHelp() for handling of topics.
|
||||
*/
|
||||
|
||||
const hook: Hook<'command-not-found'> = async function (
|
||||
opts: object & { config: IConfig; id?: string },
|
||||
opts: object & { config: IConfig; id?: string; argv?: string[] },
|
||||
) {
|
||||
const Levenshtein = await import('fast-levenshtein');
|
||||
const _ = await import('lodash');
|
||||
@ -44,16 +46,36 @@ const hook: Hook<'command-not-found'> = async function (
|
||||
return _.minBy(commandIDs, (c) => Levenshtein.get(cmd, c))!;
|
||||
}
|
||||
|
||||
const suggestions: string[] = [];
|
||||
suggestions.push(closest(commandId).replace(':', ' ') || '');
|
||||
|
||||
// opts.argv contains everything after the first command word
|
||||
// if there's something there, also test if it might be a double
|
||||
// word command spelt wrongly, rather than command args.
|
||||
if (opts.argv?.[0]) {
|
||||
suggestions.unshift(
|
||||
closest(`${commandId}: + ${opts.argv[0]}`).replace(':', ' ') || '',
|
||||
);
|
||||
}
|
||||
|
||||
// Output suggestions
|
||||
console.error(
|
||||
`${color.yellow(command)} is not a recognized balena command.\n`,
|
||||
);
|
||||
|
||||
const suggestion = closest(commandId).replace(':', ' ') || '';
|
||||
console.log(`Did you mean: ${color.cmd(suggestion)} ? `);
|
||||
console.log(
|
||||
`Run ${color.cmd('balena help -v')} for a list of available commands.`,
|
||||
console.error(`Did you mean: ? `);
|
||||
suggestions.forEach((s) => {
|
||||
console.error(` ${color.cmd(s)}`);
|
||||
});
|
||||
console.error(
|
||||
`\nRun ${color.cmd('balena help -v')} for a list of available commands,`,
|
||||
);
|
||||
console.error(
|
||||
` or ${color.cmd(
|
||||
'balena help <command>',
|
||||
)} for detailed help on a specific command.`,
|
||||
);
|
||||
|
||||
// Exit
|
||||
const COMMAND_NOT_FOUND = 127;
|
||||
process.exit(COMMAND_NOT_FOUND);
|
||||
};
|
||||
|
@ -28,9 +28,7 @@ export const trackPromise = new Promise((resolve) => {
|
||||
* parsed by oclif, but before the command's run() function is called.
|
||||
* See: https://oclif.io/docs/hooks
|
||||
*
|
||||
* This hook is used to track CLI command signatures with mixpanel. This
|
||||
* is the oclif version of what is already done for Capitano commands.
|
||||
*
|
||||
* This hook is used to track CLI command signatures with mixpanel.
|
||||
* A command signature is something like "env add NAME [VALUE]". That's
|
||||
* literally so: 'NAME' and 'VALUE' are NOT replaced with actual values.
|
||||
*/
|
||||
|
@ -70,7 +70,7 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
||||
let args = cmdSlice;
|
||||
|
||||
// Convert space separated subcommands (e.g. `end add`), to colon-separated format (e.g. `env:add`)
|
||||
if (isSubcommand(cmdSlice)) {
|
||||
if (await isSubcommand(cmdSlice)) {
|
||||
// convert space-separated commands to oclif's topic:command syntax
|
||||
args = [cmdSlice[0] + ':' + cmdSlice[1], ...cmdSlice.slice(2)];
|
||||
Logger.command = `${cmdSlice[0]} ${cmdSlice[1]}`;
|
||||
@ -136,72 +136,8 @@ export function checkDeletedCommand(argvSlice: string[]): void {
|
||||
|
||||
// Check if this is a space separated 'topic command' style command subcommand (e.g. `end add`)
|
||||
// by comparing with oclif style colon-separated subcommand list (e.g. `env:add`)
|
||||
// TODO: Need to find a way of doing this that does not require maintaining list of IDs
|
||||
export function isSubcommand(args: string[]) {
|
||||
return oclifCommandIds.includes(`${args[0] || ''}:${args[1] || ''}`);
|
||||
export async function isSubcommand(args: string[]) {
|
||||
const { getCommandIdsFromManifest } = await import('./utils/oclif-utils');
|
||||
const commandIds = await getCommandIdsFromManifest();
|
||||
return commandIds.includes(`${args[0] || ''}:${args[1] || ''}`);
|
||||
}
|
||||
|
||||
export const oclifCommandIds = [
|
||||
'api-key:generate',
|
||||
'app',
|
||||
'app:create',
|
||||
'app:restart',
|
||||
'app:rm',
|
||||
'apps',
|
||||
'build',
|
||||
'config:generate',
|
||||
'config:inject',
|
||||
'config:read',
|
||||
'config:reconfigure',
|
||||
'config:write',
|
||||
'deploy',
|
||||
'device',
|
||||
'device:identify',
|
||||
'device:init',
|
||||
'device:move',
|
||||
'device:os-update',
|
||||
'device:public-url',
|
||||
'device:reboot',
|
||||
'device:register',
|
||||
'device:rename',
|
||||
'device:rm',
|
||||
'device:shutdown',
|
||||
'devices',
|
||||
'devices:supported',
|
||||
'envs',
|
||||
'env:add',
|
||||
'env:rename',
|
||||
'env:rm',
|
||||
'help',
|
||||
'internal:scandevices',
|
||||
'internal:osinit',
|
||||
'join',
|
||||
'keys',
|
||||
'key',
|
||||
'key:add',
|
||||
'key:rm',
|
||||
'leave',
|
||||
'local:configure',
|
||||
'local:flash',
|
||||
'login',
|
||||
'logout',
|
||||
'logs',
|
||||
'note',
|
||||
'os:build-config',
|
||||
'os:configure',
|
||||
'os:versions',
|
||||
'os:download',
|
||||
'os:initialize',
|
||||
'preload',
|
||||
'push',
|
||||
'scan',
|
||||
'settings',
|
||||
'ssh',
|
||||
'tags',
|
||||
'tag:rm',
|
||||
'tag:set',
|
||||
'tunnel',
|
||||
'util:available-drives',
|
||||
'version',
|
||||
'whoami',
|
||||
];
|
||||
|
103
lib/utils/bootstrap.ts
Normal file
103
lib/utils/bootstrap.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @license
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* THIS MODULE SHOULD NOT IMPORT / REQUIRE ANYTHING AT THE GLOBAL LEVEL.
|
||||
* It is meant to contain elementary helper functions or classes that
|
||||
* can be used very early on during CLI startup, before anything else
|
||||
* like Sentry error reporting, preparser, oclif parser and the like.
|
||||
*/
|
||||
|
||||
export class CliSettings {
|
||||
public readonly settings: any;
|
||||
constructor() {
|
||||
this.settings = require('balena-settings-client') as typeof import('balena-settings-client');
|
||||
}
|
||||
|
||||
public get<T>(name: string): T {
|
||||
return this.settings.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like settings.get(), but return `undefined` instead of throwing an
|
||||
* error if the setting is not found / not defined.
|
||||
*/
|
||||
public getCatch<T>(name: string): T | undefined {
|
||||
try {
|
||||
return this.settings.get(name);
|
||||
} catch (err) {
|
||||
if (!/Setting not found/i.test(err.message)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseBoolEnvVar(varName: string): boolean {
|
||||
return !['0', 'no', 'false', '', undefined].includes(
|
||||
process.env[varName]?.toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeEnvVar(varName: string) {
|
||||
process.env[varName] = parseBoolEnvVar(varName) ? '1' : '';
|
||||
}
|
||||
|
||||
const bootstrapVars = ['DEBUG', 'BALENARC_NO_SENTRY'];
|
||||
|
||||
export function normalizeEnvVars(varNames: string[] = bootstrapVars) {
|
||||
for (const varName of varNames) {
|
||||
normalizeEnvVar(varName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export 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);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error executing pkgExec "${modFunc}" [${args.join()}]`);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
4
lib/utils/compose-types.d.ts
vendored
4
lib/utils/compose-types.d.ts
vendored
@ -30,6 +30,8 @@ export interface BuiltImage {
|
||||
dockerfile?: string;
|
||||
projectType?: string;
|
||||
size?: number;
|
||||
startTime?: Date;
|
||||
endTime?: Date;
|
||||
};
|
||||
serviceName: string;
|
||||
}
|
||||
@ -64,7 +66,7 @@ export interface ComposeCliFlags {
|
||||
'multi-dockerignore': boolean;
|
||||
nogitignore: boolean;
|
||||
'noparent-check': boolean;
|
||||
'registry-secrets'?: string | RegistrySecrets;
|
||||
'registry-secrets'?: RegistrySecrets;
|
||||
'convert-eol': boolean;
|
||||
'noconvert-eol': boolean;
|
||||
projectName?: string;
|
||||
|
@ -17,90 +17,7 @@
|
||||
|
||||
import * as path from 'path';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getChalk, stripIndent } from './lazy';
|
||||
|
||||
export function appendOptions(opts) {
|
||||
return opts.concat([
|
||||
{
|
||||
signature: 'emulated',
|
||||
description: 'Run an emulated build using Qemu',
|
||||
boolean: true,
|
||||
alias: 'e',
|
||||
},
|
||||
{
|
||||
signature: 'dockerfile',
|
||||
parameter: 'Dockerfile',
|
||||
description:
|
||||
'Alternative Dockerfile name/path, relative to the source folder',
|
||||
},
|
||||
{
|
||||
signature: 'logs',
|
||||
description:
|
||||
'No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.',
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'nologs',
|
||||
description:
|
||||
'Hide the image build log output (produce less verbose output)',
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'gitignore',
|
||||
alias: 'g',
|
||||
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.`,
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'multi-dockerignore',
|
||||
alias: 'm',
|
||||
description:
|
||||
'Have each service use its own .dockerignore file. See "balena help build".',
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'nogitignore',
|
||||
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
|
||||
boolean: true,
|
||||
alias: 'G',
|
||||
},
|
||||
{
|
||||
signature: 'noparent-check',
|
||||
description:
|
||||
"Disable project validation check of 'docker-compose.yml' file in parent folder",
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'registry-secrets',
|
||||
alias: 'R',
|
||||
parameter: 'secrets.yml|.json',
|
||||
description:
|
||||
'Path to a YAML or JSON file with passwords for a private Docker registry',
|
||||
},
|
||||
{
|
||||
signature: 'convert-eol',
|
||||
description: 'No-op and deprecated since balena CLI v12.0.0',
|
||||
boolean: true,
|
||||
alias: 'l',
|
||||
},
|
||||
{
|
||||
signature: 'noconvert-eol',
|
||||
description:
|
||||
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).",
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'projectName',
|
||||
parameter: 'projectName',
|
||||
description:
|
||||
'Specify an alternate project name; default is the directory name',
|
||||
alias: 'n',
|
||||
},
|
||||
]);
|
||||
}
|
||||
import { getChalk } from './lazy';
|
||||
|
||||
/**
|
||||
* @returns Promise<{import('./compose-types').ComposeOpts}>
|
||||
@ -168,23 +85,6 @@ export function createProject(composePath, composeStr, projectName = null) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tar stream out of the local filesystem at the given directory,
|
||||
* while optionally applying file filters such as '.dockerignore' and
|
||||
* optionally converting text file line endings (CRLF to LF).
|
||||
* @param {string} dir Source directory
|
||||
* @param {import('./compose-types').TarDirectoryOptions} param
|
||||
* @returns {Promise<import('stream').Readable>}
|
||||
*/
|
||||
export function tarDirectory(dir, param) {
|
||||
let { nogitignore = false } = param;
|
||||
if (nogitignore) {
|
||||
return require('./compose_ts').tarDirectory(dir, param);
|
||||
} else {
|
||||
return originalTarDirectory(dir, param);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the CLI v10 / v11 "original" tarDirectory function. It is still
|
||||
* around for the benefit of the `--gitignore` option, but is expected to be
|
||||
@ -193,7 +93,7 @@ export function tarDirectory(dir, param) {
|
||||
* @param {import('./compose-types').TarDirectoryOptions} param
|
||||
* @returns {Promise<import('stream').Readable>}
|
||||
*/
|
||||
function originalTarDirectory(dir, param) {
|
||||
export async function originalTarDirectory(dir, param) {
|
||||
let {
|
||||
preFinalizeCallback = null,
|
||||
convertEol = false,
|
||||
@ -268,265 +168,6 @@ function originalTarDirectory(dir, param) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {number} len
|
||||
* @returns {string}
|
||||
*/
|
||||
const truncateString = function (str, len) {
|
||||
if (str.length < len) {
|
||||
return str;
|
||||
}
|
||||
str = str.slice(0, len);
|
||||
// return everything up to the last line. this is a cheeky way to avoid
|
||||
// having to deal with splitting the string midway through some special
|
||||
// character sequence.
|
||||
return str.slice(0, str.lastIndexOf('\n'));
|
||||
};
|
||||
|
||||
const LOG_LENGTH_MAX = 512 * 1024; // 512KB
|
||||
|
||||
export function buildProject(
|
||||
docker,
|
||||
logger,
|
||||
projectPath,
|
||||
projectName,
|
||||
composition,
|
||||
arch,
|
||||
deviceType,
|
||||
emulated,
|
||||
buildOpts,
|
||||
inlineLogs,
|
||||
convertEol,
|
||||
dockerfilePath,
|
||||
nogitignore,
|
||||
multiDockerignore,
|
||||
) {
|
||||
const Bluebird = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const humanize = require('humanize');
|
||||
const compose = require('resin-compose-parse');
|
||||
const builder = require('resin-multibuild');
|
||||
const transpose = require('docker-qemu-transpose');
|
||||
const { BALENA_ENGINE_TMP_PATH } = require('../config');
|
||||
const {
|
||||
checkBuildSecretsRequirements,
|
||||
makeBuildTasks,
|
||||
} = require('./compose_ts');
|
||||
const qemu = require('./qemu');
|
||||
const { toPosixPath } = builder.PathUtils;
|
||||
|
||||
logger.logInfo(`Building for ${arch}/${deviceType}`);
|
||||
|
||||
const imageDescriptors = compose.parse(composition);
|
||||
const imageDescriptorsByServiceName = _.keyBy(
|
||||
imageDescriptors,
|
||||
'serviceName',
|
||||
);
|
||||
|
||||
let renderer;
|
||||
if (inlineLogs) {
|
||||
renderer = new BuildProgressInline(
|
||||
logger.streams['build'],
|
||||
imageDescriptors,
|
||||
);
|
||||
} else {
|
||||
const tty = require('./tty')(process.stdout);
|
||||
renderer = new BuildProgressUI(tty, imageDescriptors);
|
||||
}
|
||||
renderer.start();
|
||||
|
||||
return Bluebird.resolve(checkBuildSecretsRequirements(docker, projectPath))
|
||||
.then(() => qemu.installQemuIfNeeded(emulated, logger, arch, docker))
|
||||
.tap(function (needsQemu) {
|
||||
if (!needsQemu) {
|
||||
return;
|
||||
}
|
||||
logger.logInfo('Emulation is enabled');
|
||||
// Copy qemu into all build contexts
|
||||
return Promise.all(
|
||||
imageDescriptors.map(function (d) {
|
||||
if (typeof d.image === 'string' || d.image.context == null) {
|
||||
return;
|
||||
}
|
||||
// external image
|
||||
return qemu.copyQemu(path.join(projectPath, d.image.context), arch);
|
||||
}),
|
||||
);
|
||||
})
|
||||
.then((
|
||||
needsQemu, // Tar up the directory, ready for the build stream
|
||||
) =>
|
||||
Bluebird.resolve(
|
||||
tarDirectory(projectPath, {
|
||||
composition,
|
||||
convertEol,
|
||||
multiDockerignore,
|
||||
nogitignore,
|
||||
}),
|
||||
)
|
||||
.then((tarStream) =>
|
||||
makeBuildTasks(
|
||||
composition,
|
||||
tarStream,
|
||||
{ arch, deviceType },
|
||||
logger,
|
||||
projectName,
|
||||
),
|
||||
)
|
||||
.map(function (/** @type {any} */ task) {
|
||||
const d = imageDescriptorsByServiceName[task.serviceName];
|
||||
|
||||
// multibuild parses the composition internally so any tags we've
|
||||
// set before are lost; re-assign them here
|
||||
task.tag ??= [projectName, task.serviceName].join('_').toLowerCase();
|
||||
|
||||
if (typeof d.image !== 'string' && d.image.context != null) {
|
||||
d.image.tag = task.tag;
|
||||
}
|
||||
|
||||
// configure build opts appropriately
|
||||
task.dockerOpts ??= {};
|
||||
|
||||
_.merge(task.dockerOpts, buildOpts, { t: task.tag });
|
||||
if (typeof d.image !== 'string') {
|
||||
/** @type {any} */
|
||||
const context = d.image.context;
|
||||
if (context?.args != null) {
|
||||
task.dockerOpts.buildargs ??= {};
|
||||
_.merge(task.dockerOpts.buildargs, context.args);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the service-specific log stream
|
||||
// Caveat: `multibuild.BuildTask` defines no `logStream` property
|
||||
// but it's convenient to store it there; it's JS ultimately.
|
||||
task.logStream = renderer.streams[task.serviceName];
|
||||
task.logBuffer = [];
|
||||
|
||||
// Setup emulation if needed
|
||||
if (task.external || !needsQemu) {
|
||||
return [task, null];
|
||||
}
|
||||
const binPath = qemu.qemuPathInContext(
|
||||
path.join(projectPath, task.context ?? ''),
|
||||
);
|
||||
if (task.buildStream == null) {
|
||||
throw new Error(`No buildStream for task '${task.tag}'`);
|
||||
}
|
||||
return transpose
|
||||
.transposeTarStream(
|
||||
task.buildStream,
|
||||
{
|
||||
hostQemuPath: toPosixPath(binPath),
|
||||
containerQemuPath: `/tmp/${qemu.QEMU_BIN_NAME}`,
|
||||
qemuFileMode: 0o555,
|
||||
},
|
||||
dockerfilePath || undefined,
|
||||
)
|
||||
.then((/** @type {any} */ stream) => {
|
||||
task.buildStream = stream;
|
||||
})
|
||||
.return([task, binPath]);
|
||||
}),
|
||||
)
|
||||
.map(function ([task, qemuPath]) {
|
||||
const captureStream = buildLogCapture(task.external, task.logBuffer);
|
||||
|
||||
if (task.external) {
|
||||
// External image -- there's no build to be performed,
|
||||
// just follow pull progress.
|
||||
captureStream.pipe(task.logStream);
|
||||
task.progressHook = pullProgressAdapter(captureStream);
|
||||
} else {
|
||||
task.streamHook = function (stream) {
|
||||
let rawStream;
|
||||
stream = createLogStream(stream);
|
||||
if (qemuPath != null) {
|
||||
const buildThroughStream = transpose.getBuildThroughStream({
|
||||
hostQemuPath: toPosixPath(qemuPath),
|
||||
containerQemuPath: `/tmp/${qemu.QEMU_BIN_NAME}`,
|
||||
});
|
||||
rawStream = stream.pipe(buildThroughStream);
|
||||
} else {
|
||||
rawStream = stream;
|
||||
}
|
||||
// `stream` sends out raw strings in contrast to `task.progressHook`
|
||||
// where we're given objects. capture these strings as they come
|
||||
// before we parse them.
|
||||
return rawStream
|
||||
.pipe(dropEmptyLinesStream())
|
||||
.pipe(captureStream)
|
||||
.pipe(buildProgressAdapter(inlineLogs))
|
||||
.pipe(task.logStream);
|
||||
};
|
||||
}
|
||||
return task;
|
||||
})
|
||||
.then(function (tasks) {
|
||||
logger.logDebug('Prepared tasks; building...');
|
||||
return builder
|
||||
.performBuilds(tasks, docker, BALENA_ENGINE_TMP_PATH)
|
||||
.then(function (builtImages) {
|
||||
return Promise.all(
|
||||
builtImages.map(function (builtImage) {
|
||||
if (!builtImage.successful) {
|
||||
/** @type {Error & {serviceName?: string}} */
|
||||
const error = builtImage.error ?? new Error();
|
||||
error.serviceName = builtImage.serviceName;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const d = imageDescriptorsByServiceName[builtImage.serviceName];
|
||||
const task = _.find(tasks, {
|
||||
serviceName: builtImage.serviceName,
|
||||
});
|
||||
|
||||
const image = {
|
||||
serviceName: d.serviceName,
|
||||
name: typeof d.image === 'string' ? d.image : d.image.tag,
|
||||
logs: truncateString(task.logBuffer.join('\n'), LOG_LENGTH_MAX),
|
||||
props: {
|
||||
dockerfile: builtImage.dockerfile,
|
||||
projectType: builtImage.projectType,
|
||||
},
|
||||
};
|
||||
|
||||
// Times here are timestamps, so test whether they're null
|
||||
// before creating a date out of them, as `new Date(null)`
|
||||
// creates a date representing UNIX time 0.
|
||||
if (builtImage.startTime) {
|
||||
image.props.startTime = new Date(builtImage.startTime);
|
||||
}
|
||||
if (builtImage.endTime) {
|
||||
image.props.endTime = new Date(builtImage.endTime);
|
||||
}
|
||||
return docker
|
||||
.getImage(image.name)
|
||||
.inspect()
|
||||
.get('Size')
|
||||
.then((size) => {
|
||||
image.props.size = size;
|
||||
})
|
||||
.return(image);
|
||||
}),
|
||||
);
|
||||
})
|
||||
.then(function (images) {
|
||||
const summary = _(images)
|
||||
.map(({ serviceName, props }) => [
|
||||
serviceName,
|
||||
`Image size: ${humanize.filesize(props.size)}`,
|
||||
])
|
||||
.fromPairs()
|
||||
.value();
|
||||
renderer.end(summary);
|
||||
return images;
|
||||
});
|
||||
})
|
||||
.finally(renderer.end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} apiEndpoint
|
||||
* @param {string} auth
|
||||
@ -724,8 +365,7 @@ export const pushAndUpdateServiceImages = function (
|
||||
Promise.all(
|
||||
images.map(({ serviceImage, localImage, props, logs }, index) =>
|
||||
Promise.all([
|
||||
// @ts-ignore
|
||||
localImage.inspect().get('Size'),
|
||||
localImage.inspect().then((img) => img.Size),
|
||||
retry(
|
||||
// @ts-ignore
|
||||
() => progress.push(localImage.name, reporters[index], opts),
|
||||
@ -791,102 +431,7 @@ var pushProgressRenderer = function (tty, prefix) {
|
||||
return fn;
|
||||
};
|
||||
|
||||
var createLogStream = function (input) {
|
||||
const split = require('split');
|
||||
const stripAnsi = require('strip-ansi-stream');
|
||||
return input.pipe(stripAnsi()).pipe(split());
|
||||
};
|
||||
|
||||
var dropEmptyLinesStream = function () {
|
||||
const through = require('through2');
|
||||
return through(function (data, _enc, cb) {
|
||||
const str = data.toString('utf-8');
|
||||
if (str.trim()) {
|
||||
this.push(str);
|
||||
}
|
||||
return cb();
|
||||
});
|
||||
};
|
||||
|
||||
var buildLogCapture = function (objectMode, buffer) {
|
||||
const through = require('through2');
|
||||
|
||||
return through({ objectMode }, function (data, _enc, cb) {
|
||||
// data from pull stream
|
||||
if (data.error) {
|
||||
buffer.push(`${data.error}`);
|
||||
} else if (data.progress && data.status) {
|
||||
buffer.push(`${data.progress}% ${data.status}`);
|
||||
} else if (data.status) {
|
||||
buffer.push(`${data.status}`);
|
||||
|
||||
// data from build stream
|
||||
} else {
|
||||
buffer.push(data);
|
||||
}
|
||||
|
||||
return cb(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
var buildProgressAdapter = function (inline) {
|
||||
const through = require('through2');
|
||||
|
||||
const stepRegex = /^\s*Step\s+(\d+)\/(\d+)\s*: (.+)$/;
|
||||
|
||||
let step = null;
|
||||
let numSteps = null;
|
||||
let progress;
|
||||
|
||||
return through({ objectMode: true }, function (str, _enc, cb) {
|
||||
if (str == null) {
|
||||
return cb(null, str);
|
||||
}
|
||||
|
||||
if (inline) {
|
||||
return cb(null, { status: str });
|
||||
}
|
||||
|
||||
if (/^Successfully tagged /.test(str)) {
|
||||
progress = undefined;
|
||||
} else {
|
||||
const match = stepRegex.exec(str);
|
||||
if (match) {
|
||||
step = match[1];
|
||||
numSteps ??= match[2];
|
||||
str = match[3];
|
||||
}
|
||||
if (step != null) {
|
||||
str = `Step ${step}/${numSteps}: ${str}`;
|
||||
progress = Math.floor(
|
||||
(parseInt(step, 10) * 100) / parseInt(numSteps, 10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null, { status: str, progress });
|
||||
});
|
||||
};
|
||||
|
||||
var pullProgressAdapter = (outStream) =>
|
||||
function ({ status, id, percentage, error, errorDetail }) {
|
||||
if (status != null) {
|
||||
status = status.replace(/^Status: /, '');
|
||||
}
|
||||
if (id != null) {
|
||||
status = `${id}: ${status}`;
|
||||
}
|
||||
if (percentage === 100) {
|
||||
percentage = undefined;
|
||||
}
|
||||
return outStream.write({
|
||||
status,
|
||||
progress: percentage,
|
||||
error: errorDetail?.message ?? error,
|
||||
});
|
||||
};
|
||||
|
||||
class BuildProgressUI {
|
||||
export class BuildProgressUI {
|
||||
constructor(tty, descriptors) {
|
||||
this._handleEvent = this._handleEvent.bind(this);
|
||||
this._handleInterrupt = this._handleInterrupt.bind(this);
|
||||
@ -1061,7 +606,7 @@ class BuildProgressUI {
|
||||
}
|
||||
}
|
||||
|
||||
class BuildProgressInline {
|
||||
export class BuildProgressInline {
|
||||
constructor(outStream, descriptors) {
|
||||
this.start = this.start.bind(this);
|
||||
this.end = this.end.bind(this);
|
||||
@ -1112,7 +657,7 @@ class BuildProgressInline {
|
||||
|
||||
if (summary != null) {
|
||||
this._services.forEach((service) => {
|
||||
this._renderEvent(service, summary[service]);
|
||||
this._renderEvent(service, { status: summary[service] });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -14,17 +14,23 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import { BalenaSDK } from 'balena-sdk';
|
||||
import type { TransposeOptions } from 'docker-qemu-transpose';
|
||||
import type * as Dockerode from 'dockerode';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { Composition } from 'resin-compose-parse';
|
||||
import type {
|
||||
BuildConfig,
|
||||
Composition,
|
||||
ImageDescriptor,
|
||||
} from 'resin-compose-parse';
|
||||
import type * as MultiBuild from 'resin-multibuild';
|
||||
import type { Readable } from 'stream';
|
||||
import type { Duplex, Readable } from 'stream';
|
||||
import type { Pack } from 'tar-stream';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
||||
import {
|
||||
BuiltImage,
|
||||
ComposeCliFlags,
|
||||
@ -34,16 +40,9 @@ import {
|
||||
TaggedImage,
|
||||
TarDirectoryOptions,
|
||||
} from './compose-types';
|
||||
import { DeviceInfo } from './device/api';
|
||||
import type { DeviceInfo } from './device/api';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
export interface RegistrySecrets {
|
||||
[registryAddress: string]: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
|
||||
const exists = async (filename: string) => {
|
||||
try {
|
||||
@ -54,8 +53,8 @@ const exists = async (filename: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const LOG_LENGTH_MAX = 512 * 1024; // 512KB
|
||||
const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml'];
|
||||
|
||||
const hr =
|
||||
'----------------------------------------------------------------------';
|
||||
|
||||
@ -131,6 +130,372 @@ async function resolveProject(
|
||||
return [composeFileName, composeFileContents];
|
||||
}
|
||||
|
||||
interface BuildTaskPlus extends MultiBuild.BuildTask {
|
||||
logBuffer?: string[];
|
||||
}
|
||||
|
||||
interface Renderer {
|
||||
start: () => void;
|
||||
end: (buildSummaryByService?: Dictionary<string>) => void;
|
||||
streams: Dictionary<NodeJS.ReadWriteStream>;
|
||||
}
|
||||
|
||||
export async function buildProject(opts: {
|
||||
docker: Dockerode;
|
||||
logger: Logger;
|
||||
projectPath: string;
|
||||
projectName: string;
|
||||
composition: Composition;
|
||||
arch: string;
|
||||
deviceType: string;
|
||||
emulated: boolean;
|
||||
buildOpts: import('./docker').BuildOpts;
|
||||
inlineLogs?: boolean;
|
||||
convertEol: boolean;
|
||||
dockerfilePath?: string;
|
||||
nogitignore: boolean;
|
||||
multiDockerignore: boolean;
|
||||
}): Promise<BuiltImage[]> {
|
||||
const { logger, projectName } = opts;
|
||||
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
|
||||
|
||||
let buildSummaryByService: Dictionary<string> | undefined;
|
||||
const compose = await import('resin-compose-parse');
|
||||
const imageDescriptors = compose.parse(opts.composition);
|
||||
const imageDescriptorsByServiceName = _.keyBy(
|
||||
imageDescriptors,
|
||||
'serviceName',
|
||||
);
|
||||
const renderer = await startRenderer({ imageDescriptors, ...opts });
|
||||
try {
|
||||
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
|
||||
|
||||
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
|
||||
|
||||
const tarStream = await tarDirectory(opts.projectPath, opts);
|
||||
|
||||
const tasks: BuildTaskPlus[] = await makeBuildTasks(
|
||||
opts.composition,
|
||||
tarStream,
|
||||
opts,
|
||||
logger,
|
||||
projectName,
|
||||
);
|
||||
|
||||
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
|
||||
|
||||
const transposeOptArray: Array<
|
||||
TransposeOptions | undefined
|
||||
> = await Promise.all(
|
||||
tasks.map((task) => {
|
||||
// Setup emulation if needed
|
||||
if (needsQemu && !task.external) {
|
||||
return qemuTransposeBuildStream({ task, ...opts });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
// transposeOptions may be undefined. That's OK.
|
||||
transposeOptArray.map((transposeOptions, index) =>
|
||||
setTaskProgressHooks({
|
||||
task: tasks[index],
|
||||
renderer,
|
||||
transposeOptions,
|
||||
...opts,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
logger.logDebug('Prepared tasks; building...');
|
||||
|
||||
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
|
||||
const builder = await import('resin-multibuild');
|
||||
|
||||
const builtImages = await builder.performBuilds(
|
||||
tasks,
|
||||
opts.docker,
|
||||
BALENA_ENGINE_TMP_PATH,
|
||||
);
|
||||
|
||||
const [images, summaryMsgByService] = await inspectBuiltImages({
|
||||
builtImages,
|
||||
imageDescriptorsByServiceName,
|
||||
tasks,
|
||||
...opts,
|
||||
});
|
||||
buildSummaryByService = summaryMsgByService;
|
||||
|
||||
return images;
|
||||
} finally {
|
||||
renderer.end(buildSummaryByService);
|
||||
}
|
||||
}
|
||||
|
||||
async function startRenderer({
|
||||
imageDescriptors,
|
||||
inlineLogs,
|
||||
logger,
|
||||
}: {
|
||||
imageDescriptors: ImageDescriptor[];
|
||||
inlineLogs?: boolean;
|
||||
logger: Logger;
|
||||
}): Promise<Renderer> {
|
||||
let renderer: Renderer;
|
||||
if (inlineLogs) {
|
||||
renderer = new (await import('./compose')).BuildProgressInline(
|
||||
logger.streams['build'],
|
||||
imageDescriptors,
|
||||
);
|
||||
} else {
|
||||
const tty = (await import('./tty'))(process.stdout);
|
||||
renderer = new (await import('./compose')).BuildProgressUI(
|
||||
tty,
|
||||
imageDescriptors,
|
||||
);
|
||||
}
|
||||
renderer.start();
|
||||
return renderer;
|
||||
}
|
||||
|
||||
async function installQemuIfNeeded({
|
||||
arch,
|
||||
docker,
|
||||
emulated,
|
||||
imageDescriptors,
|
||||
logger,
|
||||
projectPath,
|
||||
}: {
|
||||
arch: string;
|
||||
docker: Dockerode;
|
||||
emulated: boolean;
|
||||
imageDescriptors: ImageDescriptor[];
|
||||
logger: Logger;
|
||||
projectPath: string;
|
||||
}): Promise<boolean> {
|
||||
const qemu = await import('./qemu');
|
||||
const needsQemu = await qemu.installQemuIfNeeded(
|
||||
emulated,
|
||||
logger,
|
||||
arch,
|
||||
docker,
|
||||
);
|
||||
if (needsQemu) {
|
||||
logger.logInfo('Emulation is enabled');
|
||||
// Copy qemu into all build contexts
|
||||
await Promise.all(
|
||||
imageDescriptors.map(function (d) {
|
||||
if (isBuildConfig(d.image)) {
|
||||
return qemu.copyQemu(
|
||||
path.join(projectPath, d.image.context || '.'),
|
||||
arch,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
return needsQemu;
|
||||
}
|
||||
|
||||
function setTaskAttributes({
|
||||
tasks,
|
||||
buildOpts,
|
||||
imageDescriptorsByServiceName,
|
||||
projectName,
|
||||
}: {
|
||||
tasks: BuildTaskPlus[];
|
||||
buildOpts: import('./docker').BuildOpts;
|
||||
imageDescriptorsByServiceName: Dictionary<ImageDescriptor>;
|
||||
projectName: string;
|
||||
}) {
|
||||
for (const task of tasks) {
|
||||
const d = imageDescriptorsByServiceName[task.serviceName];
|
||||
// multibuild (splitBuildStream) parses the composition internally so
|
||||
// any tags we've set before are lost; re-assign them here
|
||||
task.tag ??= [projectName, task.serviceName].join('_').toLowerCase();
|
||||
if (isBuildConfig(d.image)) {
|
||||
d.image.tag = task.tag;
|
||||
}
|
||||
// reassign task.args so that the `--buildArg` flag takes precedence
|
||||
// over assignments in the docker-compose.yml file (service.build.args)
|
||||
task.args = {
|
||||
...task.args,
|
||||
...buildOpts.buildargs,
|
||||
};
|
||||
|
||||
// Docker image build options
|
||||
task.dockerOpts ??= {};
|
||||
if (task.args && Object.keys(task.args).length) {
|
||||
task.dockerOpts.buildargs = {
|
||||
...task.dockerOpts.buildargs,
|
||||
...task.args,
|
||||
};
|
||||
}
|
||||
_.merge(task.dockerOpts, buildOpts, { t: task.tag });
|
||||
}
|
||||
}
|
||||
|
||||
async function qemuTransposeBuildStream({
|
||||
task,
|
||||
dockerfilePath,
|
||||
projectPath,
|
||||
}: {
|
||||
task: BuildTaskPlus;
|
||||
dockerfilePath?: string;
|
||||
projectPath: string;
|
||||
}): Promise<TransposeOptions> {
|
||||
const qemu = await import('./qemu');
|
||||
const binPath = qemu.qemuPathInContext(
|
||||
path.join(projectPath, task.context ?? ''),
|
||||
);
|
||||
if (task.buildStream == null) {
|
||||
throw new Error(`No buildStream for task '${task.tag}'`);
|
||||
}
|
||||
|
||||
const transpose = await import('docker-qemu-transpose');
|
||||
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
|
||||
|
||||
const transposeOptions: TransposeOptions = {
|
||||
hostQemuPath: toPosixPath(binPath),
|
||||
containerQemuPath: `/tmp/${qemu.QEMU_BIN_NAME}`,
|
||||
qemuFileMode: 0o555,
|
||||
};
|
||||
|
||||
task.buildStream = (await transpose.transposeTarStream(
|
||||
task.buildStream,
|
||||
transposeOptions,
|
||||
dockerfilePath || undefined,
|
||||
)) as Pack;
|
||||
|
||||
return transposeOptions;
|
||||
}
|
||||
|
||||
async function setTaskProgressHooks({
|
||||
inlineLogs,
|
||||
renderer,
|
||||
task,
|
||||
transposeOptions,
|
||||
}: {
|
||||
inlineLogs?: boolean;
|
||||
renderer: Renderer;
|
||||
task: BuildTaskPlus;
|
||||
transposeOptions?: import('docker-qemu-transpose').TransposeOptions;
|
||||
}) {
|
||||
const transpose = await import('docker-qemu-transpose');
|
||||
// Get the service-specific log stream
|
||||
const logStream = renderer.streams[task.serviceName];
|
||||
task.logBuffer = [];
|
||||
const captureStream = buildLogCapture(task.external, task.logBuffer);
|
||||
|
||||
if (task.external) {
|
||||
// External image -- there's no build to be performed,
|
||||
// just follow pull progress.
|
||||
captureStream.pipe(logStream);
|
||||
task.progressHook = pullProgressAdapter(captureStream);
|
||||
} else {
|
||||
task.streamHook = function (stream) {
|
||||
let rawStream;
|
||||
stream = createLogStream(stream);
|
||||
if (transposeOptions) {
|
||||
const buildThroughStream = transpose.getBuildThroughStream(
|
||||
transposeOptions,
|
||||
);
|
||||
rawStream = stream.pipe(buildThroughStream);
|
||||
} else {
|
||||
rawStream = stream;
|
||||
}
|
||||
// `stream` sends out raw strings in contrast to `task.progressHook`
|
||||
// where we're given objects. capture these strings as they come
|
||||
// before we parse them.
|
||||
return rawStream
|
||||
.pipe(dropEmptyLinesStream())
|
||||
.pipe(captureStream)
|
||||
.pipe(buildProgressAdapter(!!inlineLogs))
|
||||
.pipe(logStream);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function inspectBuiltImages({
|
||||
builtImages,
|
||||
docker,
|
||||
imageDescriptorsByServiceName,
|
||||
tasks,
|
||||
}: {
|
||||
builtImages: MultiBuild.LocalImage[];
|
||||
docker: Dockerode;
|
||||
imageDescriptorsByServiceName: Dictionary<ImageDescriptor>;
|
||||
tasks: BuildTaskPlus[];
|
||||
}): Promise<[BuiltImage[], Dictionary<string>]> {
|
||||
const images: BuiltImage[] = await Promise.all(
|
||||
builtImages.map((builtImage: MultiBuild.LocalImage) =>
|
||||
inspectBuiltImage({
|
||||
builtImage,
|
||||
docker,
|
||||
imageDescriptorsByServiceName,
|
||||
tasks,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const humanize = require('humanize');
|
||||
const summaryMsgByService: { [serviceName: string]: string } = {};
|
||||
for (const image of images) {
|
||||
summaryMsgByService[image.serviceName] = `Image size: ${humanize.filesize(
|
||||
image.props.size,
|
||||
)}`;
|
||||
}
|
||||
|
||||
return [images, summaryMsgByService];
|
||||
}
|
||||
|
||||
async function inspectBuiltImage({
|
||||
builtImage,
|
||||
docker,
|
||||
imageDescriptorsByServiceName,
|
||||
tasks,
|
||||
}: {
|
||||
builtImage: MultiBuild.LocalImage;
|
||||
docker: Dockerode;
|
||||
imageDescriptorsByServiceName: Dictionary<ImageDescriptor>;
|
||||
tasks: BuildTaskPlus[];
|
||||
}): Promise<BuiltImage> {
|
||||
if (!builtImage.successful) {
|
||||
const error: Error & { serviceName?: string } =
|
||||
builtImage.error ?? new Error();
|
||||
error.serviceName = builtImage.serviceName;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const d = imageDescriptorsByServiceName[builtImage.serviceName];
|
||||
const task = _.find(tasks, {
|
||||
serviceName: builtImage.serviceName,
|
||||
});
|
||||
|
||||
const image: BuiltImage = {
|
||||
serviceName: d.serviceName,
|
||||
name: (isBuildConfig(d.image) ? d.image.tag : d.image) || '',
|
||||
logs: truncateString(task?.logBuffer?.join('\n') || '', LOG_LENGTH_MAX),
|
||||
props: {
|
||||
dockerfile: builtImage.dockerfile,
|
||||
projectType: builtImage.projectType,
|
||||
},
|
||||
};
|
||||
|
||||
// Times here are timestamps, so test whether they're null
|
||||
// before creating a date out of them, as `new Date(null)`
|
||||
// creates a date representing UNIX time 0.
|
||||
if (builtImage.startTime) {
|
||||
image.props.startTime = new Date(builtImage.startTime);
|
||||
}
|
||||
if (builtImage.endTime) {
|
||||
image.props.endTime = new Date(builtImage.endTime);
|
||||
}
|
||||
image.props.size = (await docker.getImage(image.name).inspect()).Size;
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the ".balena/balena.yml" file (or resin.yml, or yaml or json),
|
||||
* which contains "build metadata" for features like "build secrets" and
|
||||
@ -178,16 +543,14 @@ async function loadBuildMetatada(
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a map of service name to service subdirectory, obtained from the given
|
||||
* composition object. If a composition object is not provided, an attempt will
|
||||
* be made to parse a 'docker-compose.yml' file at the given sourceDir.
|
||||
* Entries will be NOT be returned for subdirectories equal to '.' (e.g. the
|
||||
* 'main' "service" of a single-container application).
|
||||
*
|
||||
* Return a map of service name to service subdirectory (relative to sourceDir),
|
||||
* obtained from the given composition object. If a composition object is not
|
||||
* provided, an attempt will be made to parse a 'docker-compose.yml' file at
|
||||
* the given sourceDir.
|
||||
* @param sourceDir Project source directory (project root)
|
||||
* @param composition Optional previously parsed composition object
|
||||
*/
|
||||
async function getServiceDirsFromComposition(
|
||||
export async function getServiceDirsFromComposition(
|
||||
sourceDir: string,
|
||||
composition?: Composition,
|
||||
): Promise<Dictionary<string>> {
|
||||
@ -207,9 +570,9 @@ async function getServiceDirsFromComposition(
|
||||
const relPrefix = '.' + path.sep;
|
||||
for (const [serviceName, service] of Object.entries(composition.services)) {
|
||||
let dir =
|
||||
typeof service.build === 'string'
|
||||
(typeof service.build === 'string'
|
||||
? service.build
|
||||
: service.build?.context || '.';
|
||||
: service.build?.context) || '.';
|
||||
// Convert forward slashes to backslashes on Windows
|
||||
dir = path.normalize(dir);
|
||||
// Make sure the path is relative to the project directory
|
||||
@ -220,25 +583,65 @@ async function getServiceDirsFromComposition(
|
||||
dir = dir.endsWith(path.sep) ? dir.slice(0, -1) : dir;
|
||||
// remove './' prefix (or '.\\' on Windows)
|
||||
dir = dir.startsWith(relPrefix) ? dir.slice(2) : dir;
|
||||
// filter out a '.' service directory (e.g. for the 'main' service
|
||||
// of a single-container application)
|
||||
if (dir && dir !== '.') {
|
||||
serviceDirs[serviceName] = dir;
|
||||
}
|
||||
|
||||
serviceDirs[serviceName] = dir || '.';
|
||||
}
|
||||
}
|
||||
return serviceDirs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if `image` is actually a docker-compose.yml `services.service.build`
|
||||
* configuration object, rather than an "external image" (`services.service.image`).
|
||||
*
|
||||
* The `image` argument may therefore refere to either a `build` or `image` property
|
||||
* of a service in a docker-compose.yml file, which is a bit confusing but it matches
|
||||
* the `ImageDescriptor.image` property as defined by `resin-compose-parse`.
|
||||
*
|
||||
* Note that `resin-compose-parse` "normalizes" the docker-compose.yml file such
|
||||
* that, if `services.service.build` is a string, it is converted to a BuildConfig
|
||||
* object with the string value assigned to `services.service.build.context`:
|
||||
* https://github.com/balena-io-modules/resin-compose-parse/blob/v2.1.3/src/compose.ts#L166-L167
|
||||
* This is why this implementation works when `services.service.build` is defined
|
||||
* as a string in the docker-compose.yml file.
|
||||
*
|
||||
* @param image The `ImageDescriptor.image` attribute parsed with `resin-compose-parse`
|
||||
*/
|
||||
export function isBuildConfig(
|
||||
image: string | BuildConfig,
|
||||
): image is BuildConfig {
|
||||
return image != null && typeof image !== 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tar stream out of the local filesystem at the given directory,
|
||||
* while optionally applying file filters such as '.dockerignore' and
|
||||
* optionally converting text file line endings (CRLF to LF).
|
||||
* @param dir Source directory
|
||||
* @param param Options
|
||||
* @returns {Promise<import('stream').Readable>}
|
||||
* @returns Readable stream
|
||||
*/
|
||||
export async function tarDirectory(
|
||||
dir: string,
|
||||
param: TarDirectoryOptions,
|
||||
): Promise<import('stream').Readable> {
|
||||
const { nogitignore = false } = param;
|
||||
if (nogitignore) {
|
||||
return newTarDirectory(dir, param);
|
||||
} else {
|
||||
return (await import('./compose')).originalTarDirectory(dir, param);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tar stream out of the local filesystem at the given directory,
|
||||
* while optionally applying file filters such as '.dockerignore' and
|
||||
* optionally converting text file line endings (CRLF to LF).
|
||||
* @param dir Source directory
|
||||
* @param param Options
|
||||
* @returns Readable stream
|
||||
*/
|
||||
async function newTarDirectory(
|
||||
dir: string,
|
||||
{
|
||||
composition,
|
||||
@ -252,10 +655,6 @@ export async function tarDirectory(
|
||||
const { filterFilesWithDockerignore } = await import('./ignore');
|
||||
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
|
||||
|
||||
const serviceDirs = multiDockerignore
|
||||
? await getServiceDirsFromComposition(dir, composition)
|
||||
: {};
|
||||
|
||||
let readFile: (file: string) => Promise<Buffer>;
|
||||
if (process.platform === 'win32') {
|
||||
const { readFileWithEolConversion } = require('./eol-conversion');
|
||||
@ -265,10 +664,11 @@ export async function tarDirectory(
|
||||
}
|
||||
const tar = await import('tar-stream');
|
||||
const pack = tar.pack();
|
||||
const serviceDirs = await getServiceDirsFromComposition(dir, composition);
|
||||
const {
|
||||
filteredFileList,
|
||||
dockerignoreFiles,
|
||||
} = await filterFilesWithDockerignore(dir, serviceDirs);
|
||||
} = await filterFilesWithDockerignore(dir, multiDockerignore, serviceDirs);
|
||||
printDockerignoreWarn(dockerignoreFiles, serviceDirs, multiDockerignore);
|
||||
for (const fileStats of filteredFileList) {
|
||||
pack.entry(
|
||||
@ -295,7 +695,7 @@ export async function tarDirectory(
|
||||
* @param serviceDirsByService Map of service names to service subdirectories
|
||||
* @param multiDockerignore Whether --multi-dockerignore (-m) was provided
|
||||
*/
|
||||
export function printDockerignoreWarn(
|
||||
function printDockerignoreWarn(
|
||||
dockerignoreFiles: Array<import('./ignore').FileStats>,
|
||||
serviceDirsByService: Dictionary<string>,
|
||||
multiDockerignore: boolean,
|
||||
@ -441,7 +841,7 @@ export async function checkBuildSecretsRequirements(
|
||||
export async function getRegistrySecrets(
|
||||
sdk: BalenaSDK,
|
||||
inputFilename?: string,
|
||||
): Promise<RegistrySecrets> {
|
||||
): Promise<MultiBuild.RegistrySecrets> {
|
||||
if (inputFilename != null) {
|
||||
return await parseRegistrySecrets(inputFilename);
|
||||
}
|
||||
@ -464,7 +864,7 @@ export async function getRegistrySecrets(
|
||||
|
||||
async function parseRegistrySecrets(
|
||||
secretsFilename: string,
|
||||
): Promise<RegistrySecrets> {
|
||||
): Promise<MultiBuild.RegistrySecrets> {
|
||||
try {
|
||||
let isYaml = false;
|
||||
if (/.+\.ya?ml$/i.test(secretsFilename)) {
|
||||
@ -661,7 +1061,7 @@ async function validateSpecifiedDockerfile(
|
||||
|
||||
export interface ProjectValidationResult {
|
||||
dockerfilePath: string;
|
||||
registrySecrets: RegistrySecrets;
|
||||
registrySecrets: MultiBuild.RegistrySecrets;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -797,7 +1197,7 @@ async function pushServiceImages(
|
||||
export async function deployProject(
|
||||
docker: import('docker-toolbelt'),
|
||||
logger: Logger,
|
||||
composition: import('resin-compose-parse').Composition,
|
||||
composition: Composition,
|
||||
images: BuiltImage[],
|
||||
appId: number,
|
||||
userId: number,
|
||||
@ -907,9 +1307,127 @@ export function createRunLoop(tick: (...args: any[]) => void) {
|
||||
return runloop;
|
||||
}
|
||||
|
||||
function createLogStream(input: Readable) {
|
||||
const split = require('split') as typeof import('split');
|
||||
const stripAnsi = require('strip-ansi-stream');
|
||||
return input.pipe<Duplex>(stripAnsi()).pipe(split());
|
||||
}
|
||||
|
||||
function dropEmptyLinesStream() {
|
||||
const through = require('through2') as typeof import('through2');
|
||||
return through(function (data, _enc, cb) {
|
||||
const str = data.toString('utf-8');
|
||||
if (str.trim()) {
|
||||
this.push(str);
|
||||
}
|
||||
return cb();
|
||||
});
|
||||
}
|
||||
|
||||
function buildLogCapture(objectMode: boolean, buffer: string[]) {
|
||||
const through = require('through2') as typeof import('through2');
|
||||
|
||||
return through({ objectMode }, function (data, _enc, cb) {
|
||||
// data from pull stream
|
||||
if (data.error) {
|
||||
buffer.push(`${data.error}`);
|
||||
} else if (data.progress && data.status) {
|
||||
buffer.push(`${data.progress}% ${data.status}`);
|
||||
} else if (data.status) {
|
||||
buffer.push(`${data.status}`);
|
||||
|
||||
// data from build stream
|
||||
} else {
|
||||
buffer.push(data);
|
||||
}
|
||||
|
||||
return cb(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
function buildProgressAdapter(inline: boolean) {
|
||||
const through = require('through2') as typeof import('through2');
|
||||
|
||||
const stepRegex = /^\s*Step\s+(\d+)\/(\d+)\s*: (.+)$/;
|
||||
|
||||
let step = '';
|
||||
let numSteps = '';
|
||||
let progress: number | undefined;
|
||||
|
||||
return through({ objectMode: true }, function (str, _enc, cb) {
|
||||
if (str == null) {
|
||||
return cb(null, str);
|
||||
}
|
||||
|
||||
if (inline) {
|
||||
return cb(null, { status: str });
|
||||
}
|
||||
|
||||
if (!/^Successfully tagged /.test(str)) {
|
||||
const match = stepRegex.exec(str);
|
||||
if (match) {
|
||||
step = match[1];
|
||||
numSteps ??= match[2];
|
||||
str = match[3];
|
||||
}
|
||||
if (step) {
|
||||
str = `Step ${step}/${numSteps}: ${str}`;
|
||||
progress = Math.floor(
|
||||
(parseInt(step, 10) * 100) / parseInt(numSteps, 10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null, { status: str, progress });
|
||||
});
|
||||
}
|
||||
|
||||
function pullProgressAdapter(outStream: Duplex) {
|
||||
return function ({
|
||||
status,
|
||||
id,
|
||||
percentage,
|
||||
error,
|
||||
errorDetail,
|
||||
}: {
|
||||
status: string;
|
||||
id: string;
|
||||
percentage: number | undefined;
|
||||
error: Error;
|
||||
errorDetail: Error;
|
||||
}) {
|
||||
if (status != null) {
|
||||
status = status.replace(/^Status: /, '');
|
||||
}
|
||||
if (id != null) {
|
||||
status = `${id}: ${status}`;
|
||||
}
|
||||
if (percentage === 100) {
|
||||
percentage = undefined;
|
||||
}
|
||||
return outStream.write({
|
||||
status,
|
||||
progress: percentage,
|
||||
error: errorDetail?.message ?? error,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function truncateString(str: string, len: number): string {
|
||||
if (str.length < len) {
|
||||
return str;
|
||||
}
|
||||
str = str.slice(0, len);
|
||||
// return everything up to the last line. this is a cheeky way to avoid
|
||||
// having to deal with splitting the string midway through some special
|
||||
// character sequence.
|
||||
return str.slice(0, str.lastIndexOf('\n'));
|
||||
}
|
||||
|
||||
export const composeCliFlags: flags.Input<ComposeCliFlags> = {
|
||||
emulated: flags.boolean({
|
||||
description: 'Run an emulated build using Qemu',
|
||||
description:
|
||||
'Use QEMU for ARM architecture emulation during the image build',
|
||||
char: 'e',
|
||||
}),
|
||||
dockerfile: flags.string({
|
||||
|
@ -68,7 +68,7 @@ export async function generateBaseConfig(
|
||||
};
|
||||
|
||||
const config = (await getBalenaSdk().models.os.getConfig(
|
||||
application.app_name,
|
||||
application.slug,
|
||||
options,
|
||||
)) as ImgConfig & { apiKey?: string };
|
||||
// os.getConfig always returns a config for an app
|
||||
|
@ -30,13 +30,18 @@ const getBuilderLogPushEndpoint = function (baseUrl, buildId, owner, app) {
|
||||
return `https://builder.${baseUrl}/v1/pushLogs?${args}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('docker-toolbelt')} docker
|
||||
* @param {string} imageId
|
||||
* @param {string} bufferFile
|
||||
*/
|
||||
const bufferImage = function (docker, imageId, bufferFile) {
|
||||
const streamUtils = require('./streams');
|
||||
|
||||
const image = docker.getImage(imageId);
|
||||
const imageMetadata = image.inspect();
|
||||
const sizePromise = image.inspect().then((img) => img.Size);
|
||||
|
||||
return Promise.all([image.get(), imageMetadata.get('Size')]).then(
|
||||
return Promise.all([image.get(), sizePromise]).then(
|
||||
([imageStream, imageSize]) =>
|
||||
streamUtils.buffer(imageStream, bufferFile).then((bufferedStream) => {
|
||||
// @ts-ignore adding an extra property
|
||||
@ -150,14 +155,17 @@ const uploadLogs = function (logs, token, url, buildId, username, appName) {
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
opts must be a hash with the following keys:
|
||||
|
||||
- appName: the name of the app to deploy to
|
||||
- imageName: the name of the image to deploy
|
||||
- buildLogs: a string with build output
|
||||
- shouldUploadLogs
|
||||
*/
|
||||
/**
|
||||
* @param {import('docker-toolbelt')} docker
|
||||
* @param {import('./logger')} logger
|
||||
* @param {string} token
|
||||
* @param {string} username
|
||||
* @param {string} url
|
||||
* @param {{appName: string; imageName: string; buildLogs: string; shouldUploadLogs: boolean}} opts
|
||||
* - appName: the name of the app to deploy to
|
||||
* - imageName: the name of the image to deploy
|
||||
* - buildLogs: a string with build output
|
||||
*/
|
||||
export const deployLegacy = async function (
|
||||
docker,
|
||||
logger,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user