Compare commits

...

542 Commits

Author SHA1 Message Date
a3cab32b4e v12.0.0 2020-06-16 11:02:40 +03:00
d709e06f48 Merge pull request #1867 from balena-io/1770-major-sdk-pkg-proxy
Release CLI v12
2020-06-16 08:00:33 +00:00
98f101643d v12 RELEASE NOTES: see https://git.io/Jf7hz
Full URL: https://github.com/balena-io/balena-cli/wiki/CLI-v12-Release-Notes

Change-type: patch
2020-06-16 00:30:58 +01:00
c619bd4b99 Update 'balena-lint' and apply new prettier rules
Change-type: patch
2020-06-15 23:53:07 +01:00
19c3069b22 Convert 'logs' command to async/await and add tests
Change-type: patch
2020-06-15 23:53:05 +01:00
7e1d58546c Add tests for standalone executable via proxy server
Change-type: patch
2020-06-15 23:53:04 +01:00
2c01f8adee Update 'global-agent' (fix proxy server issues with unauthenticated setup)
Change-type: patch
2020-06-15 23:53:02 +01:00
3ecf461d55 Update 'balena-sdk' from v12 to v13 and update code and tests as needed
Change-type: patch
2020-06-15 23:53:01 +01:00
06ab84fd10 Update 'pkg' dependency (improve support for Node v14)
Change-type: patch
2020-06-15 23:52:59 +01:00
a7b78d2ccd Turn v12 feature switch on
See https://github.com/balena-io/balena-cli/issues/1770

Change-type: major
2020-06-15 23:52:58 +01:00
432109060e Update minimum Node.js requirement from v8 to v10
Change-type: major
2020-06-15 23:52:54 +01:00
b32ae4a667 v11.36.0 2020-06-11 20:04:33 +03:00
36e4b3249c Merge pull request #1859 from balena-io/1857-device-mac_address
balena device: Add the mac_address field
2020-06-11 17:02:28 +00:00
41e5fdbe27 balena device: Add the mac_address field
Connects-to: #1857
HQ: https://github.com/balena-io/balena/issues/2195
Depends-on: https://github.com/balena-io/balena-sdk/pull/914
Change-type: minor
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2020-06-11 18:54:53 +03:00
6dc0fe10bc v11.35.21 2020-06-11 18:39:32 +03:00
656591fde4 Merge pull request #1864 from balena-io/preconfigure-device-name
config: Allow setting the initialDeviceName when configuring an OS image
2020-06-11 15:37:58 +00:00
d967b942e0 Allow setting the initialDeviceName
Change-type: patch
Signed-off-by: Rich Bayliss <rich@balena.io>
2020-06-11 16:02:17 +01:00
7e34fdfeeb v11.35.20 2020-06-10 16:07:09 +03:00
995f8a3338 Merge pull request #1863 from balena-io/remove-string-error-handling
Restrict error handler typing
2020-06-10 15:05:39 +02:00
ff282205d5 Restrict error handler typing
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-06-10 12:37:09 +02:00
be144fafa2 v11.35.19 2020-06-10 09:25:32 +03:00
683037cd2f Merge pull request #1862 from balena-io/fix-balenaexpiredtoken-handling
Fix handling of BalenaExpiredToken error
2020-06-10 08:23:46 +02:00
555096db6b Fix handling of BalenaExpiredToken error
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-06-09 17:40:06 +02:00
a85c482416 v11.35.18 2020-06-05 22:26:41 +03:00
523d563b4e Merge pull request #1856 from balena-io/default-convert-eol
v12 preparations: Add feature switch for default eol-converson
2020-06-05 19:25:04 +00:00
1569915fae v12 preparations: Add feature switch for default eol-converson
Change-type: patch
Connects-to: #1770
2020-06-05 16:37:44 +01:00
b1552f8e9b v12 preparations: Fix dockerignore tests on Windows
Change-type: patch
2020-06-05 16:35:07 +01:00
f455602c73 v11.35.17 2020-06-02 23:04:51 +03:00
3e97669b3c Merge pull request #1854 from balena-io/convert-devices-publicurl
Convert 'balena devices public-url' commands to oclif
2020-06-02 20:01:40 +00:00
728c4f4296 Convert 'balena device public-url' commands to oclif
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-06-02 19:00:32 +02:00
bf073942f0 v11.35.16 2020-06-02 19:27:43 +03:00
b9290f4859 Merge pull request #1855 from balena-io/1770-build-deploy-logs-nologs
v12 preparations: Add feature switch for build/deploy `--logs` option
2020-06-02 16:25:47 +00:00
626d328194 v12 preparations: Add feature switch for build/deploy --logs option
Connects-to: #1770
Change-type: patch
2020-06-02 16:42:51 +01:00
21dd959344 v11.35.15 2020-06-01 10:47:04 +03:00
5c8d822aee Merge pull request #1853 from balena-io/1770-devices-supported-et-al
v12 preparations: Add feature switches ('devices supported' and others)
2020-06-01 07:45:32 +00:00
2ab8ae1c10 v12 preparations: Add feature switch for project directory validation
Connects-to: #1770
Change-type: patch
2020-05-29 22:03:55 +01:00
fcc13f9476 v12 preparations: Add feature switch for 'balena apps --verbose'
Connects-to: #1770
Change-type: patch
2020-05-29 22:03:39 +01:00
a38b41f339 v12 preparations: Add feature switch for 'devices supported' default columns
Connects-to: #1770
Change-type: patch
2020-05-29 20:06:14 +01:00
2fc0728a09 v12 preparations: Amend test cases for '--nogitignore' option
Connects-to: #1770
Change-type: patch
2020-05-29 20:06:14 +01:00
040c4987fc v11.35.14 2020-05-29 19:59:53 +03:00
254d9c49a4 Merge pull request #1851 from balena-io/envs-all-as-default
v12 preparations: Add feature switch for 'envs --all'
2020-05-29 16:57:00 +00:00
6e5e1c4f5f v12 preparations: Add feature switch for 'envs --all'
Change-type: patch
Connects-to: #1770
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-29 17:39:40 +02:00
d7213e868f v11.35.13 2020-05-29 18:33:19 +03:00
d82b019480 Merge pull request #1852 from balena-io/remove-id-from-tags-output
v12 preparations: Add feature switch to remove id from 'tags' output
2020-05-29 15:31:20 +00:00
1693bd91c0 v12 preparations: Add feature switch to remove id from 'tags' output
Change-type: patch
Connects-to: #1770
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-29 17:07:53 +02:00
e4f605d6ac v11.35.12 2020-05-29 10:03:49 +03:00
fd7e7f57eb Merge pull request #1850 from balena-io/1770-nogitignore-feature-switch
v12 preparations: Add feature switch for '--nogitignore'
2020-05-29 07:02:15 +00:00
1d073af31a v12 preparations: Add feature switch for '--nogitignore'
Connects-to: #1770
Change-type: patch
2020-05-29 01:12:15 +01:00
fcaaec1fff v11.35.11 2020-05-29 02:38:31 +03:00
ac3a688d46 Merge pull request #1841 from balena-io/convert-tags
Convert `tags`, `tag set`, `tag rm` to oclif.
2020-05-28 23:35:47 +00:00
979284b071 Convert tags, tag set, tag rm to oclif.
Change-type: patch
Resolves: #1805
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-28 19:49:23 +02:00
bc4aa6006e v11.35.10 2020-05-27 19:57:29 +03:00
2cad44915b Merge pull request #1845 from balena-io/change-login-message
v12 preparations - Add versionOverride function, change login message
2020-05-27 16:55:44 +00:00
889c7b08cf v12 preparations: Add version switch, update login message.
Change-type: patch
Connects-to: #1770
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-27 18:29:36 +02:00
56a196210d v11.35.9 2020-05-25 20:01:01 +03:00
a23759a1ba Merge pull request #1840 from balena-io/fix-deploy-authorize-token
balena deploy: Fix "access denied" pushing images to registry
2020-05-25 16:59:24 +00:00
ba0024645d balena deploy: Fix "access denied" pushing images to registry
Change-type: patch
2020-05-25 17:10:17 +01:00
3cb184c8af v11.35.8 2020-05-25 18:10:31 +03:00
644d54a113 Merge pull request #1839 from balena-io/fix-lazy-loading
Fix lazy loading in utils/compose
2020-05-25 15:08:32 +00:00
a6f905b71c Fix lazy loading in utils/compose
Change-type: patch
2020-05-25 15:35:03 +01:00
3b426e4a53 v11.35.7 2020-05-25 10:30:29 +03:00
d241523d93 Merge pull request #1838 from balena-io/remove-dns-workaround
Replace windows dns workaround with single lookup
2020-05-25 09:28:26 +02:00
1c354c800b Replace windows dns workaround with single lookup
Change-type: patch
Connects-to: #1518
Resolves: #1727
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-22 12:14:58 +00:00
e5861a708e v11.35.6 2020-05-22 15:13:49 +03:00
6a019af25f Merge pull request #1837 from balena-io/1045-deploy-typescript
Convert selected functions to Typescript and async/await (compose.js)
2020-05-22 13:12:12 +01:00
8522363cd3 Convert selected functions to Typescript and async/await (compose.js)
Connects-to: #1045
Change-type: patch
2020-05-22 00:24:46 +01:00
480228d8f4 Add tests for 'balena deploy'
Connects-to: #1045
Change-type: patch
2020-05-22 00:24:46 +01:00
175413af34 v11.35.5 2020-05-21 14:27:10 +03:00
d1b4560b37 Merge pull request #1834 from balena-io/preserve-file-stats
Fix caching by preserving all file stats when pushing to device or cloud
2020-05-21 07:24:57 -04:00
77f3fa4b6c Fix caching by preserving all file stats when pushing to device or cloud
Change-type: patch
Signed-off-by: Cameron Diver <cameron@balena.io>
2020-05-21 12:01:34 +01:00
a21d3fe2d2 v11.35.4 2020-05-19 17:40:08 +03:00
df440f0580 Merge pull request #1827 from balena-io/errors-tests
Add unit tests for errors module
2020-05-19 16:38:18 +02:00
92bfa574e3 Add unit tests for errors module
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
Resolves: #1807
2020-05-19 12:39:46 +02:00
d33b7ec585 v11.35.3 2020-05-18 16:31:06 +03:00
08d5a77734 Merge pull request #1826 from balena-io/update-typescript
Update typescript to 3.9
2020-05-18 14:28:26 +01:00
744122b1b8 Update typescript to 3.9
Change-type: patch
2020-05-18 13:32:15 +01:00
c3a8bb3de6 v11.35.2 2020-05-18 13:14:46 +03:00
e50d92727e Merge pull request #1829 from balena-io/1404-1710-web-login
Fix 'balena login' web authorization hanging with Google Chrome
2020-05-18 11:13:03 +01:00
3bb5e495a6 Fix 'balena login' web authorization hanging with Google Chrome
Resolves: #1404
Change-type: patch
2020-05-16 23:32:07 +01:00
803a9070fd Update web page wording for 'balena login' web authorization
Resolves: #1710
Change-type: patch
2020-05-16 00:01:09 +01:00
a84ab793a0 Update balena preload help message (clarify accepted image formats)
Connects-to: #1810
Change-type: patch
2020-05-16 00:00:22 +01:00
81269e92d5 Update pre-commit script error message (automation/check-doc.js)
Change-type: patch
2020-05-15 21:38:23 +01:00
11d5deef4c v11.35.1 2020-05-14 17:58:20 +03:00
c98bc3280d Merge pull request #1823 from balena-io/issue-template
Update GitHub templates for new issues and pull requests
2020-05-14 10:55:21 -04:00
8c2a40cb39 Update GitHub templates for new issues and pull requests
Change-type: patch
2020-05-14 15:25:45 +01:00
2fdd023a64 v11.35.0 2020-05-14 16:19:51 +03:00
aff370e9c3 Merge pull request #1825 from balena-io/1824-app-exists
`app create`: "You already have an application with that name!"
2020-05-14 09:15:18 -04:00
be21c8d43e balena apps: add --verbose option to list application slugs (full app name)
Change-type: minor
2020-05-14 11:15:51 +01:00
5b33826309 balena app create: fix application existence check
Resolves: #1824
Change-type: patch
2020-05-14 11:15:51 +01:00
052c8d138e v11.34.0 2020-05-14 01:18:30 +03:00
d0228f20fd Merge pull request #1813 from balena-io/1032-revisit-gitignore2
Add --nogitignore flag (new treatment of .gitignore and .dockerignore files)
2020-05-13 18:11:26 -04:00
4577d72ead push/build/deploy: add --nogitignore option and update dockerignore filter library
Connects-to: #1032
Connects-to: #1148
Change-type: minor
2020-05-13 22:33:37 +01:00
0dde84ec0b v11.33.4 2020-05-13 01:17:16 +03:00
81b620f55e Merge pull request #1821 from balena-io/1820-avg-antivirus
Re-create standalone zip package (release asset) for Windows
2020-05-12 18:12:07 -04:00
4b056b4d4c Re-create standalone zip package (release asset) for Windows
Change-type: patch
2020-05-12 22:22:23 +01:00
5723e69267 v11.33.3 2020-05-12 11:47:07 +03:00
2d341cac48 Merge pull request #1818 from balena-io/fix-livepush-v3
Fix livepush v3
2020-05-12 09:45:18 +01:00
4e50d08f7b Fix usage of livepush v3 features
Change-type: patch
Signed-off-by: Cameron Diver <cameron@balena.io>
2020-05-12 09:24:05 +01:00
aff5cd9b0d v11.33.2 2020-05-12 00:37:55 +03:00
2bb0933a42 Merge pull request #1817 from balena-io/1815-numeric-app-id
Fix 'balena app' (rm, restart, info) with numeric app IDs
2020-05-11 17:35:51 -04:00
8d60cd1f92 Fix 'balena app' (rm, restart, info) with numeric app IDs
Resolves: #1815
Change-type: patch
2020-05-11 19:27:49 +01:00
9756efb539 v11.33.1 2020-05-11 21:02:19 +03:00
61ed6ff69d Merge pull request #1811 from balena-io/update-multibuild
Update resin-multibuild
2020-05-11 18:58:32 +01:00
127560fa65 Update resin-multibuild
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-11 17:55:04 +01:00
2611ea22f9 v11.33.0 2020-05-11 15:38:28 +03:00
d7021a556e Merge pull request #1809 from balena-io/1802-deprecation-policy
Add a deprecation policy
2020-05-11 15:30:30 +03:00
9412a21d40 Add a deprecation policy
Resolves: #1802
Change-type: minor
See: https://app.frontapp.com/open/cnv_7d92qx9
HQ: https://github.com/balena-io/balena/issues/2032
See: https://www.flowdock.com/app/rulemotion/r-product/threads/A6-bJBldfUFnhG9vixTz-DLz9iB
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2020-05-11 11:12:58 +03:00
f63be0b4cd v11.32.15 2020-05-06 14:12:03 +03:00
c2561938c1 Merge pull request #1806 from balena-io/errors-improvements
Improve presentation of errors, help
2020-05-06 13:09:52 +02:00
98a2c0635d Improve presentation of errors, help
Change-type: patch
Resolves: #1779 #1757
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-06 10:48:50 +02:00
ee54d638ad v11.32.14 2020-05-04 17:51:13 +03:00
d9b044c1b8 Merge pull request #1804 from balena-io/disable-oclif-ts-node
Disable oclif's ts-node registering when running against built code
2020-05-04 15:47:24 +01:00
dd20a8b00f Disable oclif's ts-node registering when running against built code
Change-type: patch
2020-05-04 15:08:24 +01:00
01147c31a4 v11.32.13 2020-05-04 15:19:27 +03:00
2f6889cca1 Merge pull request #1749 from balena-io/convert-api-key
Convert `balena api-key generate` to oclif
2020-05-04 14:17:23 +02:00
83286e6729 Convert balena api-key generate to oclif
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-04 13:50:50 +02:00
97def08ec5 v11.32.12 2020-05-04 13:15:15 +03:00
1a57385626 Merge pull request #1801 from balena-io/sentry-command
Configure the sentry command scope earlier
2020-05-04 11:13:05 +01:00
1301f62981 Configure the sentry command scope earlier
Change-type: patch
2020-05-04 10:04:44 +01:00
6ae337db8a v11.32.11 2020-05-01 19:31:39 +03:00
b84cdd6230 Merge pull request #1799 from balena-io/avoid-unnecessary-api-calls
Avoid unnecessary api calls in `balena build` and `balena deploy`
2020-05-01 17:29:44 +01:00
2f24e591ef Avoid unnecessary api calls in balena build and balena deploy
Change-type: patch
2020-05-01 15:58:44 +01:00
597b894917 v11.32.10 2020-05-01 16:07:04 +03:00
3b53b75626 Merge pull request #1791 from balena-io/errors-refactor
Errors refactor
2020-05-01 15:04:30 +02:00
9b1c3c665b Refactor: move error related functions into error module
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-01 14:41:39 +02:00
153cdf4bb0 Refactor: use checkLoggedIn() instead of exitIfNotLoggedIn()
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-01 14:40:37 +02:00
1d9a397f71 v11.32.9 2020-05-01 14:16:11 +03:00
6167c7b8b3 Merge pull request #1797 from balena-io/ts-qemu
Convert qemu.js to typescript
2020-05-01 12:13:44 +01:00
cbcd7694a9 Merge qemu-ts.ts and qemu.ts files 2020-05-01 11:43:45 +01:00
52bece7f17 Convert qemu.js to typescript
Change-type: patch
2020-05-01 11:43:44 +01:00
4f6550e7eb v11.32.8 2020-05-01 12:58:14 +03:00
ec17ed6ef2 Merge pull request #1798 from balena-io/enforce-lazy-loading
Enforce lazy loading via tslint import-blacklist
2020-05-01 10:56:17 +01:00
0df6368ab9 Enforce lazy loading via tslint import-blacklist
Change-type: patch
2020-05-01 09:33:29 +00:00
b5cac122cf v11.32.7 2020-05-01 11:25:23 +03:00
ae75e1396e Merge pull request #1752 from balena-io/convert-app
Convert app commands to oclif
2020-05-01 10:23:17 +02:00
3b519f0258 Convert app commands to oclif
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-01 08:48:58 +02:00
275fa9c16b v11.32.6 2020-05-01 08:26:26 +03:00
a00db0f5d8 Merge pull request #1790 from balena-io/modify-oclif-errors
Improve oclif missing argument/flag errors
2020-05-01 07:24:22 +02:00
2a8eb3a6ed Improve oclif missing argument/flag errors
Change-type: patch
Resolves: #1776
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-01 06:31:12 +02:00
bcd49e0292 v11.32.5 2020-05-01 07:26:14 +03:00
d8e1cd6597 Merge pull request #1781 from balena-io/modify-oclif-help
Modify oclif help to match balena conventions
2020-05-01 06:24:04 +02:00
52c2b041da Modify oclif help to match balena conventions
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-05-01 05:30:35 +02:00
d5d0486c3f v11.32.4 2020-04-30 21:33:54 +03:00
6f51807e8c Merge pull request #1793 from balena-io/js-lib-app-capitano
Convert lib/app-capitano.coffee and gulpfile.coffee to javascript
2020-04-30 19:31:42 +01:00
ab526c9ed8 Convert gulpfile.coffee to javascript
Change-type: patch
2020-04-30 17:58:13 +01:00
14c5b27cdd Convert lib/app-capitano.coffee to javascript
Change-type: patch
2020-04-30 17:57:37 +01:00
ce01ce73b1 v11.32.3 2020-04-30 19:56:04 +03:00
0f6d160b2e Merge pull request #1792 from balena-io/ts-lib-actions-index
Convert lib/actions/index.coffee to typescript
2020-04-30 17:53:29 +01:00
6d7d1956ea Convert lib/actions/index.coffee to typescript
Change-type: patch
2020-04-30 17:04:12 +01:00
692eddf43f v11.32.2 2020-04-30 18:56:15 +03:00
eb5cfecfaf Merge pull request #1771 from balena-io/js-lib-actions-deploy
Convert lib/utils/deploy.coffee to javascript
2020-04-30 16:54:11 +01:00
73d6d7b264 Convert lib/utils/deploy.coffee to javascript
Change-type: patch
2020-04-30 16:11:42 +01:00
14ced9f384 Merge pull request #1772 from balena-io/js-lib-actions-build
Convert lib/actions/build.coffee to javascript
2020-04-30 16:01:16 +01:00
4d8cd1cc46 Convert lib/actions/build.coffee to javascript
Change-type: patch
2020-04-30 15:17:18 +01:00
dbe9a727d5 v11.32.1 2020-04-30 16:49:46 +03:00
8ac65c3800 Merge pull request #1775 from balena-io/check-new-version-before-notify
Only notify of an update if the new version is actually newer
2020-04-30 14:45:25 +01:00
4ae91ef846 Only notify of an update if the new version is actually newer
Change-type: patch
2020-04-30 12:55:39 +00:00
7311cfa755 v11.32.0 2020-04-30 15:50:36 +03:00
a200bf268d Merge pull request #1787 from balena-io/integrate-new-livepush
Integrate livepush v3 and live directives
2020-04-30 14:48:29 +02:00
d398e22c58 Integrate livepush v3 and live directives
Change-type: minor
Resolves: #1784
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-30 12:25:17 +00:00
b51d2fffbb v11.31.28 2020-04-30 15:24:05 +03:00
5fef98bdf8 Merge pull request #1769 from balena-io/js-lib-utils-deploy-legacy
Convert lib/utils/deploy-legacy.coffee to javascript
2020-04-30 13:22:03 +01:00
203ccaf97b Convert lib/utils/deploy-legacy.coffee to javascript
Change-type: patch
2020-04-30 12:50:20 +01:00
a348528ed3 v11.31.27 2020-04-30 14:45:04 +03:00
04c4250fba Merge pull request #1780 from balena-io/js-lib-actions-help
Convert lib/actions/help.coffee to javascript
2020-04-30 12:42:46 +01:00
a97398950e Convert lib/actions/help.coffee to javascript
Change-type: patch
2020-04-30 10:56:48 +00:00
f55376df32 v11.31.26 2020-04-30 13:56:36 +03:00
3f285cc26d Merge pull request #1789 from balena-io/1788-update-qemu
balena build/deploy: Update QEMU version to support newer balenalib images
2020-04-30 06:54:13 -04:00
6d95c5bad5 balena build/deploy: Update QEMU version to support newer balenalib images
Change-type: patch
2020-04-30 10:47:51 +01:00
6b33f95661 v11.31.25 2020-04-30 07:36:33 +03:00
9ab34c2deb Merge pull request #1785 from balena-io/add-debug-flag
Add support for global --debug flag
2020-04-30 06:34:15 +02:00
db247307db Add support for global --debug flag
Change-type: patch
Resolves: #1777
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-29 17:25:05 +02:00
ad0b667bc7 v11.31.24 2020-04-29 18:13:56 +03:00
d98bc9fb06 Merge pull request #1786 from balena-io/1783-images-push-not-iterable
balena deploy: Fix "TypeError: images.push is not iterable"
2020-04-29 16:11:44 +01:00
74cdd80b51 balena deploy: Fix "TypeError: images.push is not iterable"
Change-type: patch
2020-04-29 15:13:51 +01:00
5c39952002 v11.31.23 2020-04-28 15:46:45 +03:00
2874a69d7d Merge pull request #1774 from balena-io/fix-unhandled-promise-rejection
Fix unhandled promise rejection when using `balena deploy`
2020-04-28 13:44:25 +01:00
6ec05e8dcf Fix unhandled promise rejection when using balena deploy
Change-type: patch
2020-04-28 13:22:15 +01:00
6602845202 v11.31.22 2020-04-25 17:36:24 +03:00
e8cd4153c7 Merge pull request #1766 from balena-io/js-lib-actions-device
Convert lib/actions/device.coffee to javascript
2020-04-25 15:34:31 +01:00
0cfa1a0dfb Convert lib/actions/device.coffee to javascript
Change-type: patch
2020-04-25 14:08:30 +00:00
00ce3ab751 v11.31.21 2020-04-25 16:57:15 +03:00
5c1323d583 Merge pull request #1767 from balena-io/add-types
Install types for modules used in javascript to improve type checking
2020-04-25 14:55:03 +01:00
d9f42b888d Install types for modules used in javascript to improve type checking
Change-type: patch
2020-04-25 12:01:05 +01:00
0db8c85fc8 v11.31.20 2020-04-25 02:19:34 +03:00
bc601d07e3 Merge pull request #1762 from balena-io/js-lib-actions-preload
Convert lib/actions/preload.coffee to javascript
2020-04-25 00:16:18 +01:00
e1a91035ae Convert lib/actions/preload.coffee to javascript
Change-type: patch
2020-04-24 22:44:10 +00:00
8dced8afe2 v11.31.19 2020-04-25 01:10:05 +03:00
ffded6736a Merge pull request #1763 from balena-io/js-lib-actions-config
Convert lib/actions/config.coffee to javascript
2020-04-24 23:07:43 +01:00
1a851f552e Convert lib/actions/config.coffee to javascript
Change-type: patch
2020-04-24 21:06:23 +00:00
68b64016ab v11.31.18 2020-04-25 00:01:35 +03:00
edac54ccfe Merge pull request #1756 from balena-io/js-lib-utils-compose
Convert lib/utils/compose.coffee to javascript
2020-04-24 21:59:35 +01:00
560b0abbe7 Convert lib/utils/compose.coffee to javascript
Change-type: patch
2020-04-24 16:17:23 +01:00
b48d238be6 v11.31.17 2020-04-24 18:04:06 +03:00
a10d5b9abe Merge pull request #1764 from balena-io/js-lib-utils-docker-coffee
Convert lib/utils/docker-coffee.coffee to javascript
2020-04-24 16:01:58 +01:00
23f2242e22 Convert lib/utils/docker-coffee.coffee to javascript
Change-type: patch
2020-04-24 15:25:20 +01:00
f8612fd748 v11.31.16 2020-04-24 17:14:18 +03:00
36446ff488 Merge pull request #1761 from balena-io/js-lib-actions-os
Convert lib/actions/os.coffee to javascript
2020-04-24 15:12:13 +01:00
a5ce0436c7 Convert lib/actions/os.coffee to javascript
Change-type: patch
2020-04-24 13:38:34 +01:00
3302e2f639 v11.31.15 2020-04-24 12:57:15 +03:00
c3c1c5fc41 Merge pull request #1755 from balena-io/js-lib-utils-qemu
Convert lib/utils/qemu.coffee to javascript
2020-04-24 10:55:10 +01:00
9f59b6dde5 Convert lib/utils/qemu.coffee to javascript
Change-type: patch
2020-04-24 10:20:15 +01:00
8be56ef092 v11.31.14 2020-04-23 19:35:54 +03:00
e9f8cadb73 Merge pull request #1744 from balena-io/os-update-overall-progress
device os-update: Refactor to use the overall_progress field
2020-04-23 19:34:00 +03:00
3e4f9f9572 device os-update: Refactor to use the overall_progress field
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2020-04-23 16:10:09 +00:00
f7d4a37060 v11.31.13 2020-04-23 14:48:05 +03:00
d0e268815a Merge pull request #1754 from balena-io/remove-unnecessary-files
Remove unnecessary files
2020-04-23 12:46:07 +01:00
c3454d3abb Remove unnecessary files
Change-type: patch
2020-04-23 11:21:38 +00:00
6b0f645094 v11.31.12 2020-04-23 14:15:45 +03:00
ada7801a0d Merge pull request #1746 from balena-io/js-lib-actions-local-configure
Convert lib/actions/local/configure.coffee to javascript
2020-04-23 12:12:15 +01:00
da5e26f37e Convert lib/actions/local/index.coffee to typescript
Change-type: patch
2020-04-23 11:30:04 +01:00
9447195c26 Convert lib/actions/local/configure.coffee to javascript
Change-type: patch
2020-04-23 11:12:02 +01:00
cd59496f11 Merge pull request #1602 from balena-io/ts-lib-utils-tty
Convert lib/utils/tty to typescript
2020-04-23 10:35:54 +01:00
9fda165d34 Convert lib/utils/tty to typescript
Change-type: patch
2020-04-23 09:42:23 +01:00
fe0ad92b43 v11.31.11 2020-04-22 18:37:33 +03:00
81c5a62380 Merge pull request #1750 from balena-io/npm-files-patches0
Avoid patch-package warning with 'npm install -g --production'
2020-04-22 16:34:57 +01:00
ebdd04ec73 Avoid patch-package warning with 'npm install -g --production'
Change-type: patch
2020-04-22 15:59:38 +01:00
e6264ced7a v11.31.10 2020-04-22 17:07:19 +03:00
247f31a3cc Merge pull request #1666 from balena-io/convert-scan-ts
Convert command `scan` to TypeScript, migrate to oclif
2020-04-22 16:05:29 +02:00
a2b761ec4b Convert command scan to TypeScript, migrate to oclif
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-22 11:13:40 +00:00
e3672bc655 v11.31.9 2020-04-22 14:12:25 +03:00
028141c0b0 Merge pull request #1739 from balena-io/1723-update-patch-package
Update patch-package (fix remaining source of seemingly random ENOENT error)
2020-04-22 12:10:14 +01:00
e3c42cf63e Update patch-package (fix remaining source of seemingly random ENOENT error)
Connects-to: #1723
Change-type: patch
2020-04-22 11:42:09 +01:00
0ae138db03 v11.31.8 2020-04-22 13:40:55 +03:00
9350af9ddf Merge pull request #1745 from balena-io/balena-release
Update to balena-release
2020-04-22 11:39:02 +01:00
88e4009e88 Update to balena-release
Change-type: patch
2020-04-21 17:39:25 +01:00
cb7692690d Merge pull request #1740 from balena-io/update-deps
Update dependencies to pick up performance improvements
2020-04-21 16:49:42 +01:00
82e17cea6a Update dependencies to pick up performance improvements
Change-type: patch
2020-04-21 15:42:56 +01:00
9ed363da9e v11.31.7 2020-04-21 12:09:47 +03:00
8aa4bd6173 Merge pull request #1738 from balena-io/update-codeowners
Update codeowners
2020-04-21 11:06:59 +02:00
5f098e7410 Update codeowners
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-21 10:40:11 +02:00
2de33d185a v11.31.6 2020-04-21 10:52:06 +03:00
66b9f5a337 Merge pull request #1736 from balena-io/validation-test-coverage
Add test coverage for validation module
2020-04-21 09:49:47 +02:00
bbcb3a702f Add test coverage for validation module
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-21 09:23:55 +02:00
57d0014e32 v11.31.5 2020-04-20 18:05:13 +03:00
8d9133e6a6 Merge pull request #1734 from balena-io/convert-keys-oclif
convert commands `key`, `keys`, `key add`, `key rm` to oclif.
2020-04-20 17:03:17 +02:00
be82bcfa63 convert commands key, keys, key add, key rm to oclif.
Also:
 - Display keys with `name` instead of `title`.
 - Check for empty key before calling API.

Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-20 16:07:39 +02:00
7c9a23451b v11.31.4 2020-04-20 16:17:14 +03:00
1319e0642b Merge pull request #1735 from balena-io/fix-instance-of
Unpin balena-sdk and review 'instanceof' usage
2020-04-20 14:15:19 +01:00
e3b6db25d8 Review CONTRIBUTING.md and add 'instanceof' usage advice
Change-type: patch
2020-04-18 02:50:00 +01:00
655534469a Review 'instanceof' usage with classes of external packages
Change-type: patch
2020-04-18 02:50:00 +01:00
a8b0573699 Unpin balena-sdk (bump balena-sdk to v12.33.0)
Change-type: patch
2020-04-18 02:50:00 +01:00
99963cbb89 v11.31.3 2020-04-17 02:40:26 +03:00
92715c3182 Merge pull request #1732 from balena-io/fix-ssh-app-not-found
Fix balena ssh "Application not found" (pin balena-sdk to v12.30.0)
2020-04-17 00:38:24 +01:00
264c8535b4 Fix balena ssh "Application not found" (pin balena-sdk to v12.30.0)
Change-type: patch
2020-04-17 00:11:33 +01:00
159ee44d7e v11.31.2 2020-04-16 03:39:14 +03:00
7e4b62c28a Merge pull request #1730 from balena-io/1723-update-is-installed-globally
Fix seemingly random ENOENT error (update 'is-installed-globally' dependency)
2020-04-16 01:37:15 +01:00
52b2ba6a30 Fix seemingly random ENOENT error (update 'is-installed-globally' dependency)
Change-type: patch
Resolves: #1723
2020-04-15 18:58:51 +01:00
cc1ba3d84e v11.31.1 2020-04-15 19:46:53 +03:00
01d05fb148 Merge pull request #1729 from balena-io/convert-device-ts
improve input validation for `key`, `key rm`
2020-04-15 18:44:55 +02:00
cff9e50a22 improve input validation for key, key rm
Change-type: patch
Resolves: #1728
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-15 17:08:25 +02:00
eba2e7e4fb v11.31.0 2020-04-15 16:48:28 +03:00
9a9d56b419 Merge pull request #1726 from balena-io/convert-device-ts
Enable CLI upgrade of dev OS versions
2020-04-15 15:46:21 +02:00
320b4864d9 device os-update: allow host OS upgrade with development balenaOS images
also:
fix `device os-update` incorrectly showing 0% progress
convert `device os-update` to use async/await

Change-type: minor
Resolves: #1725
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-15 15:07:10 +02:00
7f79451376 v11.30.17 2020-04-10 00:05:24 +03:00
68fa831843 Merge pull request #1702 from balena-io/convert-join-leave
Convert commands leave, join to oclif.
2020-04-09 23:03:30 +02:00
3aa72dde4c Convert commands join, leave to oclif.
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-09 20:50:42 +02:00
f72d78954d v11.30.16 2020-04-07 21:43:29 +03:00
cf87ca95a0 Merge pull request #1695 from balena-io/saintaardvark-patch-1
Minor grammar fix in balena ssh documentation
2020-04-07 11:41:41 -07:00
a50ca78eef Minor grammar fix in balena ssh documentation
Minor grammar fix in balena ssh documentation

Change-type: patch
Signed-off-by: Hugh Brown <hugh@balena.io>
2020-04-07 10:08:24 -07:00
4fe5a10029 v11.30.15 2020-04-03 18:01:50 +03:00
9812239862 Merge pull request #1693 from balena-io/convert-internal
Convert `internal scandevices`, `internal osinit` to typescript & oclif
2020-04-03 17:00:07 +02:00
bc3fe29624 Convert internal scandevices, internal osinit to typescript & oclif
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-03 16:39:39 +02:00
7e2ee7ab93 v11.30.14 2020-04-03 15:46:50 +03:00
f151a208e5 Merge pull request #1692 from balena-io/update-kind-of
Updated dependencies (vulnerability advisory CVE-2019-20149)
2020-04-03 13:44:57 +01:00
292ad89b7e Updated dependencies (vulnerability advisory CVE-2019-20149)
https://nvd.nist.gov/vuln/detail/CVE-2019-20149

Change-type: patch
2020-04-03 13:06:33 +01:00
c062e6e876 v11.30.13 2020-04-02 22:24:40 +03:00
dcb1c11700 Merge pull request #1694 from balena-io/fix-project-validation-deploy
Fix project directory validation for 'balena deploy' with pre-built image
2020-04-02 20:23:02 +01:00
96e28f3d45 Fix project directory validation for 'balena deploy' with pre-built image
Change-type: patch
2020-04-02 19:44:27 +01:00
ff319d67f3 v11.30.12 2020-04-01 17:18:17 +03:00
f14e44a2e8 Merge pull request #1690 from balena-io/remove-code-note
Remove unused code from balena note
2020-04-01 16:16:11 +02:00
9aa6b0bc57 Remove unused code from balena note
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-01 15:07:40 +02:00
cbe12d5be7 v11.30.11 2020-04-01 14:53:30 +03:00
c177f222ba Merge pull request #1691 from balena-io/build-improvements
Small improvements to `balena build`
2020-04-01 13:51:37 +02:00
d2fd1ec80a Check logged in for balena build if application specified
Correct eroneous -f flag in `balena build` help

Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-04-01 12:39:30 +02:00
77873cf919 v11.30.10 2020-03-31 18:55:44 +03:00
07c09c5f89 Merge pull request #1689 from balena-io/add-ssh-tty-option
balena ssh: handle exit codes and add `-t` option (force TTY allocation)
2020-03-31 16:53:51 +01:00
159cb752d1 Add '-t' option to 'balena ssh' to bypass TTY autodetection (force allocation)
Change-type: patch
2020-03-31 14:50:09 +01:00
a74f0413df Handle ssh process exit codes
Change-type: patch
2020-03-31 14:27:30 +01:00
43b1c5c24f v11.30.9 2020-03-31 14:21:28 +03:00
45e7e9cb32 Merge pull request #1677 from balena-io/js-lib-action-local-common
Convert lib/actions/local/common.coffee to javascript
2020-03-31 12:20:00 +01:00
1a71bad8bb Convert lib/actions/local/common.coffee to javascript
Change-type: patch
2020-03-31 11:57:47 +01:00
2d55df4704 v11.30.8 2020-03-31 02:36:18 +03:00
6c0b3a5e53 Merge pull request #1685 from balena-io/1681-fix-ssh-msys
Fix balena ssh to local device (MSYS/Windows and command pipe to service)
2020-03-31 00:34:00 +01:00
3e955f3a91 Update README regarding proxy server support
Change-type: patch
2020-03-30 14:45:16 +01:00
30738d93b0 Fix "the input device is not a TTY" when piping to 'balena ssh' (local device)
Change-type: patch
2020-03-30 14:45:16 +01:00
be76b8adbd Fix 'balena ssh' on MSYS Windows shell ("unexpected end of file")
Resolves: #1681
Change-type: patch
2020-03-30 14:45:16 +01:00
d6a065a230 Delete unused code (ssh.coffee)
Change-type: patch
2020-03-30 14:45:15 +01:00
7b8e86372b v11.30.7 2020-03-30 16:43:29 +03:00
bc15ad6e05 Merge pull request #1678 from balena-io/convert-settings-oclif
Convert commands `settings`, `note` to oclif
2020-03-30 15:41:48 +02:00
fcad35402a Convert command note to oclif
Add oclif support for piped input

Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-03-30 14:47:43 +02:00
49b00e18ae Convert command settings to oclif
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-03-30 14:47:43 +02:00
a6ccd87069 v11.30.6 2020-03-26 15:41:08 +02:00
0c1904fbdb Merge pull request #1680 from balena-io/1679-improve-device-osupdate-help
Clarify `balena device os-update` help re balenaCloud
2020-03-26 14:39:05 +01:00
e5d2661c96 Clarify balena device os-update help re balenaCloud
Change-type: patch
Resolves: #1679
Signed-off-by: Scott Lowe <scott@balena.io>
2020-03-26 13:59:12 +01:00
eca3e91512 v11.30.5 2020-03-25 14:57:29 +02:00
eb5ad08649 Merge pull request #1676 from balena-io/js-linting-type-checking
Use balena-lint for javascript linting and add javascript type-checking
2020-03-25 12:55:58 +00:00
b3b22d6399 Use balena-lint for javascript linting and add javascript type-checking
Change-type: patch
2020-03-25 12:12:03 +00:00
217cba819a v11.30.4 2020-03-24 20:57:00 +02:00
c8275b52c3 Merge pull request #1641 from balena-io/improve-events
Improve events
2020-03-24 18:54:55 +00:00
47e85da789 Deduplicate balenaUrl fetching in events
Change-type: patch
2020-03-24 17:35:44 +00:00
c8cade95da v11.30.3 2020-03-24 19:33:02 +02:00
a4de7143b1 Merge pull request #1656 from balena-io/type-check-tests
Add type checking for tests
2020-03-24 17:30:52 +00:00
6574745a23 Preserve symlinks for the sake of the balenaCI worker
Change-type: patch
2020-03-24 16:54:19 +00:00
1ee74df67e Add type checking for tests
Change-type: patch
2020-03-24 16:42:18 +00:00
3e1b10007a v11.30.2 2020-03-24 18:18:18 +02:00
448211e49c Merge pull request #1675 from balena-io/add-oclif-auth-support
Add support for authentication checking to oclif
2020-03-24 17:14:33 +01:00
8658104647 Add support for authentication checking to oclif
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-03-24 10:19:18 +01:00
6ec8bcddaa v11.30.1 2020-03-19 21:16:18 +02:00
d138c40ebd Merge pull request #1674 from balena-io/add-oclif-root-support
Add support for `root` property on oclif commands
2020-03-19 20:14:40 +01:00
f24c4a036c Add support for root property on oclif commands
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-03-19 19:50:17 +01:00
dabe81c31b v11.30.0 2020-03-19 17:14:26 +02:00
c2f0f9a894 Merge pull request #1673 from balena-io/support-oclif-secondary
Add support for primary/secondary oclif commands
2020-03-19 16:12:51 +01:00
46b695cf22 Add support for primary/secondary oclif commands
Change-type: minor
Signed-off-by: Scott Lowe <scott@balena.io>
2020-03-19 15:45:04 +01:00
9b79f79bac v11.29.5 2020-03-19 01:00:58 +02:00
dddfad9dec Merge pull request #1672 from balena-io/installmd-linux-wsl
INSTALL.md: emphasize the standalone zip package recommendation for WSL
2020-03-18 18:58:47 -04:00
0690554a94 INSTALL.md: emphasize the standalone zip package recommendation for WSL
Change-type: patch
2020-03-18 22:35:12 +00:00
62ea7518bc v11.29.4 2020-03-16 19:41:10 +02:00
f30e486562 Merge pull request #1668 from balena-io/native-check
Switch to native type checks
2020-03-16 17:39:13 +00:00
809a5fae25 Switch to native number check
Change-type: patch
2020-03-13 16:15:07 +00:00
eccb1bd9ad Switch to native string check
Change-type: patch
2020-03-13 16:13:21 +00:00
f859d5025a Switch to native Array.isArray instead of aliases
Change-type: patch
2020-03-13 16:09:43 +00:00
18d3ca3413 v11.29.3 2020-03-13 02:37:28 +02:00
a826f16469 Merge pull request #1664 from balena-io/remove-unused-typing
Remove unused typings
2020-03-13 00:35:45 +00:00
505c3ec7d3 Remove unused typings
Change-type: patch
2020-03-13 00:13:49 +00:00
47fa2a6151 v11.29.2 2020-03-13 01:09:53 +02:00
b4b19637f4 Merge pull request #1665 from balena-io/fix-opn-patch
Fix opn patch (npm installation warning)
2020-03-12 19:08:05 -04:00
5f552cf9a8 Fix opn patch (npm installation warning)
Change-type: patch
2020-03-12 22:34:59 +00:00
e42650f433 v11.29.1 2020-03-13 00:16:33 +02:00
731bd909d6 Merge pull request #1663 from balena-io/fix-local-flash
Fix `balena local flash`
2020-03-12 22:14:42 +00:00
2860535c45 Fix balena local flash
Change-type: patch
2020-03-12 21:34:40 +00:00
122b5a0655 v11.29.0 2020-03-12 21:11:52 +02:00
ec66c82d3f Merge pull request #1645 from balena-io/update-deps
Update dependencies
2020-03-12 19:09:58 +00:00
09a59ab03f Remove dependency on inquirer-dynamic-list 2020-03-12 18:03:10 +00:00
3d2e109e7f Update dependencies
Update balena-sdk from 12.26.7 to 12.29.1

Change-type: minor
2020-03-12 18:03:10 +00:00
26803067f1 v11.28.17 2020-03-12 20:01:37 +02:00
7dc3977e82 Merge pull request #1662 from balena-io/1658-remove-old-files-during-standalone-install
Make windows installer remove old files before installation.
2020-03-12 18:59:51 +01:00
10cbf514a2 Make windows installer remove old files before installation.
Change-type: patch
Resolves: #1658
2020-03-12 17:31:43 +01:00
e2114f73d7 v11.28.16 2020-03-12 02:55:02 +02:00
2f448951c9 Merge pull request #1653 from balena-io/1649-build-emulated-tests
Add test case for `build --emulated`
2020-03-11 20:53:18 -04:00
385d3e107b Update CONTRIBUTING.md regarding ./bin/balena-dev and oclif commands
Change-type: patch
2020-03-11 22:16:37 +00:00
d98b2fa72f Update CONTRIBUTING.md regarding Coffeescript to Typescript conversion
Change-type: patch
2020-03-11 22:12:26 +00:00
c6baa7a908 Prevent auto merge of npm-shrinkwrap.json and explain it in CONTRIBUTING.md
Change-type: patch
2020-03-11 22:11:54 +00:00
daa34feeda Add test case for build --emulated
Connects-to: #1649
Change-type: patch
2020-03-11 22:02:01 +00:00
f813dad4d9 v11.28.15 2020-03-11 13:45:11 +02:00
d7633b5f08 Merge pull request #1659 from balena-io/1657-xdg-open-ENOENT
Fix 'balena login' web auth on Linux standalone zip install (xdg-open ENOENT)
2020-03-11 07:43:09 -04:00
f44c2b777f Fix 'balena login' web auth on Linux standalone zip install (xdg-open ENOENT)
Resolves: #1657
Change-type: patch
2020-03-11 01:16:38 +00:00
bcfba693a5 v11.28.14 2020-03-10 16:54:18 +02:00
08f40c0566 Merge pull request #1654 from balena-io/1523-update-sentry
Replace Raven SDK with "new" Sentry "Unified" SDK (fix "CLI prints 'null' and exits")
2020-03-10 11:52:48 -03:00
5a80654305 Avoid Sentry reporting of selected common "expected" errors
Change-type: patch
2020-03-09 12:51:56 +00:00
d2df2c7b60 Fix occasional "CLI prints 'null' and exits" (replace old Raven/Sentry SDK)
Resolves: #1523
Connects-to: #1333
Connects-to: #1193
Change-type: patch
2020-03-09 12:51:56 +00:00
36d3d1256e Don't send the full command line to Sentry.io
Resolves: #703
Change-type: patch
2020-03-09 12:51:56 +00:00
b77cb56cd0 Fix occasionally missed command tracking request (oclif commands)
Change-type: patch
2020-03-09 12:51:56 +00:00
524397fc9b v11.28.13 2020-03-06 17:48:03 +02:00
ec73ee270b Merge pull request #1652 from balena-io/more-correct-livepush-ux
Improve the UX by only printing effective file changes in livepush
2020-03-06 15:46:23 +00:00
b83431c2e0 Improve the UX by only printing effective file changes in livepush
Change-type: patch
Signed-off-by: Cameron Diver <cameron@balena.io>
2020-03-06 15:17:02 +00:00
40c559322a v11.28.12 2020-03-06 04:56:19 +02:00
2c5cf9dab6 Merge pull request #1651 from balena-io/1649-fix-build-emulated
Fix `build --emulated` on Linux ("exec format error")
2020-03-05 23:54:10 -03:00
ca8272b477 Fix build --emulated on Linux ("exec format error")
Resolves: #1649
Change-type: patch
2020-03-06 01:51:36 +00:00
d6e7359400 v11.28.11 2020-03-02 18:47:38 +02:00
af8d7283a5 Merge pull request #1644 from balena-io/skip-proxy-on-no-config
Don't try to setup a proxy agent when there's no proxy configured
2020-03-02 16:45:42 +00:00
9470e804c0 Don't ignore BALENARC_NO_PROXY env var if HTTP(S)_PROXY env vars are defined
Change-type: patch
2020-03-02 15:11:02 +00:00
00943463a4 Use types for global-agent and global-tunnel-ng
Change-type: patch
2020-03-02 15:10:58 +00:00
3f6d770233 Remove lodash usage in proxy setup
Change-type: patch
2020-03-02 15:05:15 +00:00
c4a6086e9c Don't try to setup a proxy agent when there's no proxy configured
Change-type: patch
2020-03-02 15:04:52 +00:00
4e61c00255 v11.28.10 2020-03-02 17:01:29 +02:00
1713988e94 Merge pull request #1643 from balena-io/update-deps
Update dependencies
2020-03-02 14:59:33 +00:00
fe4e1d09d7 Update dependencies
Update balena-sdk from 12.21.1 to 12.26.7

Change-type: patch
2020-03-02 11:20:20 +00:00
766695ceef v11.28.9 2020-03-02 10:30:58 +02:00
e50a3270ba Merge pull request #1642 from balena-io/object-spread
Switch to object spreading in favor of _.assign
2020-03-02 08:29:17 +00:00
235c13bea9 Switch to object spreading in favor of _.assign
Change-type: patch
2020-02-29 22:08:10 +00:00
62e4930e5b v11.28.8 2020-02-29 02:17:58 +02:00
fb321b8c5b Merge pull request #1640 from balena-io/lazy-load-chalk
Lazy-load chalk
2020-02-29 00:16:03 +00:00
98152c0b09 Lazy-load chalk
Change-type: patch
2020-02-28 18:34:54 +00:00
0ab0e417b8 v11.28.7 2020-02-28 18:06:18 +02:00
3642943896 Merge pull request #1638 from balena-io/lazy-load-visuals
Simplify lazy-loading of resin-cli-visuals with a shared function
2020-02-28 16:04:30 +00:00
7c62e34455 Simplify lazy-loading of resin-cli-visuals with a shared function
This also avoids current unnecessary requiring of resin-cli-visuals
for every command

Change-type: patch
2020-02-28 15:29:07 +00:00
86af954f3b v11.28.6 2020-02-28 17:26:48 +02:00
0c7947e185 Merge pull request #1639 from balena-io/use-capitano-promise-support
Make use of capitano's promise support to simplify the code
2020-02-28 15:24:53 +00:00
48b281d7c6 Make use of capitano's promise support to simplify the code
Change-type: patch
2020-02-28 13:27:07 +00:00
8598223b61 v11.28.5 2020-02-28 02:14:29 +02:00
cdd67e25f0 Merge pull request #1636 from balena-io/simpler-lazy-load
Simplify lazy-loading of balena-sdk
2020-02-28 00:12:41 +00:00
eac6bb5e5c Simplify lazy-loading of balena-sdk by utilizing a shared function
This also avoids instantiating multiple balena-sdk

Change-type: patch
2020-02-27 17:17:36 +00:00
077d1db9b7 v11.28.4 2020-02-25 22:48:49 +02:00
d86f213b68 Merge pull request #1628 from balena-io/1624-transpose-specified-dockerfile
Fix build/deploy commands with QEMU emulation and alternative Dockerfile
2020-02-25 17:47:13 -03:00
cdfd1d124b Fix build/deploy commands with QEMU emulation and alternative Dockerfile name
Resolves: #1624
Change-type: patch
2020-02-25 16:42:12 -03:00
28c00696b8 Fix CONTRIBUTING markdown
Change-type: patch
2020-02-24 22:11:31 -03:00
dec570a6e2 v11.28.3 2020-02-24 17:43:44 +02:00
9067558d18 Merge pull request #1626 from balena-io/update-type-deps
Update type deps
2020-02-24 15:41:43 +00:00
4abdd71ce7 Update type deps
Change-type: patch
2020-02-24 14:15:48 +00:00
36f2f491b3 v11.28.2 2020-02-22 00:05:12 +02:00
5e750b33c3 Merge pull request #1623 from balena-io/check-cli-markdown-timestamp
Add pre-commit check for cli.markdown updates and coffeelint execution
2020-02-21 22:02:28 +00:00
03053e125f Add pre-commit check for cli.markdown updates and coffeelint execution
These checks compare the timestamps of cli.markdown with those of staged files,
effectively enforcing that 'npm run build' or 'npm test' are executed.

Change-type: patch
2020-02-21 18:22:48 -03:00
bdc7c0fa39 Fix 'test:fast' npm script definition
Change-type: patch
2020-02-21 18:22:48 -03:00
ad4981328f v11.28.1 2020-02-21 15:40:29 +02:00
3f35d6fde6 Merge pull request #1622 from balena-io/add-nested-changelogs-script
Add a script to automate nested changelogs
2020-02-21 15:38:45 +02:00
f2be811e18 Add a script to automate nested changelogs
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2020-02-21 15:18:17 +02:00
6439aa5552 v11.28.0 2020-02-20 05:01:08 +02:00
977fadab69 Merge pull request #1625 from balena-io/additional-template-vars
Update resin-multibuild and add app and release template vars
2020-02-20 09:57:54 +07:00
95c93d24da Update resin-multibuild and add app and release template vars
Change-type: minor
Signed-off-by: Cameron Diver <cameron@balena.io>
2020-02-18 10:21:12 +07:00
278d7fd02c v11.27.0 2020-02-17 17:51:58 +02:00
59b9429570 Merge pull request #1571 from balena-io/1421-validate-project-dir
Add project directory validation for balena push / build / deploy
2020-02-17 15:50:03 +00:00
9e870b08a7 Add tests for project directory validation
Change-type: patch
2020-02-17 15:19:07 +00:00
671dca8287 Add project directory validation for balena push / build / deploy commands
Change-type: minor
2020-02-17 15:19:07 +00:00
a15060e9fc Refactor 'balena push' error handling
Change-type: patch
2020-02-17 15:19:07 +00:00
0738dd1520 Add and refactor tests for push/build/deploy commands (docker-compose)
Change-type: patch
2020-02-17 15:19:07 +00:00
5dbace353d v11.26.0 2020-02-14 17:26:58 +02:00
e773549297 Merge pull request #1617 from balena-io/1616-deploy-cachefrom
Add '--cachefrom' option to balena build/deploy commands
2020-02-14 15:25:25 +00:00
a1c406a479 Add '--cache-from' option to balena build and deploy commands
It implements the same feature as the "docker build --cache-from" option.

Resolves: #1616
Change-type: minor
2020-02-13 18:43:15 +00:00
5e196b8f63 v11.25.18 2020-02-13 19:32:57 +02:00
054e59c6af Merge pull request #1615 from balena-io/1611-replace-mmmagic
Fix balena push "Segmentation fault" on Windows (replace 'mmmagic' with 'isBinaryFile')
2020-02-13 17:31:18 +00:00
88a1e413a3 Fix balena push "Segmentation fault" on Windows (replace 'mmmagic' with 'isBinaryFile')
Connects-to: #1611
Change-type: patch
2020-02-13 15:51:45 +00:00
1a74dcf4cf Re-add windows-crlf.sh to git index (fix git warnings) 2020-02-12 17:34:17 +00:00
d48672fa93 v11.25.17 2020-02-12 19:01:59 +02:00
9a7fcfffe8 Merge pull request #1601 from balena-io/ts-lib-actions-auth
Convert lib/actions/auth to typescript
2020-02-12 17:00:07 +00:00
f9ece2ce7d Convert lib/actions/auth to typescript
Change-type: patch
2020-02-12 14:26:32 +00:00
9d04e616a8 v11.25.16 2020-02-12 15:57:18 +02:00
b8c7f23443 Merge pull request #1600 from balena-io/ts-lib-auth-index
Convert lib/auth/index to typescript
2020-02-12 13:55:27 +00:00
2b04763ac0 Convert lib/auth/index to typescript
Change-type: patch
2020-02-12 13:20:36 +00:00
bff845a0e4 v11.25.15 2020-02-12 15:01:04 +02:00
5076ca7532 Merge pull request #1598 from balena-io/ts-lib-auth-server
Convert lib/auth/server to typescript
2020-02-12 12:59:07 +00:00
93ba5832d8 Convert lib/auth/server to typescript
Change-type: patch
2020-02-12 12:10:12 +00:00
af86ac73e6 v11.25.14 2020-02-12 13:56:18 +02:00
173a48eede Merge pull request #1613 from balena-io/ts-lib-actions-keys
Convert lib/actions/keys to typescript
2020-02-12 11:54:41 +00:00
a4b34c109d Convert lib/actions/keys to typescript
Change-type: patch
2020-02-10 22:22:06 +00:00
69714a646b v11.25.13 2020-02-11 00:11:34 +02:00
a41ef3764e Merge pull request #1612 from balena-io/ts-lib-actions-notes
Convert lib/actions/notes to typescript
2020-02-10 22:10:02 +00:00
f1220c6377 Convert lib/actions/notes to typescript
Change-type: patch
2020-02-10 18:31:40 +00:00
cefb3acc1f v11.25.12 2020-02-10 17:53:58 +02:00
277da3ea9c Merge pull request #1594 from balena-io/ts-lib-actions-app
Convert lib/actions/app to typescript
2020-02-10 15:52:25 +00:00
99f84c2f6a Convert lib/actions/app to typescript
Change-type: patch
2020-02-10 15:02:00 +00:00
a9c0899c32 v11.25.11 2020-02-10 16:28:18 +02:00
8d3fb8fef5 Merge pull request #1597 from balena-io/ts-lib-auth-utils
Convert lib/auth/utils to typescript
2020-02-10 14:26:48 +00:00
4de41ce3e0 Convert lib/auth/utils to typescript
Change-type: patch
2020-02-10 13:42:50 +00:00
4b8cec652a v11.25.10 2020-02-08 02:54:39 +02:00
2dd8e71adc Merge pull request #1608 from balena-io/1607-fix-mmmagic-build
Node 13 compatibility: update ext2fs module
2020-02-08 00:52:47 +00:00
05d478b759 CI builds: revert patch-package upgrade to fix patch errors
This fixes a build error caused by a recent version bump of 'patch-package':
"Patch file found for package execa which is not present at node_modules/qqjs/node_modules/execa"

Change-type: patch
2020-02-08 00:19:05 +00:00
9a7a364776 Node 13 compatibility: upgrade ext2fs module
Change-type: patch
Resolves: #1591
Signed-off-by: Scott Lowe <scott@balena.io>
2020-02-07 22:30:02 +00:00
2cb5e28258 v11.25.9 2020-02-08 00:29:18 +02:00
02e8429155 Merge pull request #1604 from balena-io/gitattributes
Add .gitattributes to check out with the correct line-ending on windows
2020-02-07 22:27:33 +00:00
467afb3de6 Add .gitattributes to check out with the correct line-ending on windows
Change-type: patch
2020-02-07 21:40:48 +00:00
324a406e7f v11.25.8 2020-02-07 23:36:09 +02:00
17bf061853 Merge pull request #1606 from balena-io/faster-tests
Speed up tests
2020-02-07 21:34:27 +00:00
6d543b79ff Merge resin-lint linting and fixing steps into one
Change-type: patch
2020-02-07 19:34:35 +00:00
85aaf77e44 Remove redundant type checking of tests
Change-type: patch
2020-02-07 19:34:35 +00:00
83c5684491 Remove duplicate type checking of automation code
Change-type: patch
2020-02-07 19:34:35 +00:00
6bc4fbb750 v11.25.7 2020-02-07 21:32:58 +02:00
1da96a0eb0 Merge pull request #1609 from balena-io/1607-fix-pkg-magic
Fix Windows standalone zip installer ("could not load any valid magic files")
2020-02-07 19:31:02 +00:00
be209f1626 Fix Windows standalone zip installer (missing mmmagic db for CRLF conversion)
Change-type: patch
2020-02-07 18:19:10 +00:00
654d1dcff8 v11.25.6 2020-02-07 13:21:48 +02:00
0a03e79d9d Merge pull request #1599 from balena-io/opn-to-open
Switch from opn to its new name of open
2020-02-07 11:20:00 +00:00
3f84045127 Switch from opn to its new name of open
Change-type: patch
2020-02-06 19:55:22 +00:00
544f8fb4bd v11.25.5 2020-02-06 20:50:18 +02:00
76997c99dc Merge pull request #1605 from balena-io/powershell-debug
Add debug instructions for powershell
2020-02-06 18:48:29 +00:00
f4525bc11e Add debug instructions for powershell
Change-type: patch
2020-02-06 18:24:53 +00:00
f732c5bf5d v11.25.4 2020-02-06 19:58:38 +02:00
2bc3348aff Merge pull request #1595 from balena-io/resin-lint-autofix
Use resin-lint for automatic lint fixing
2020-02-06 17:56:35 +00:00
895be0be5d Use resin-lint for automatic lint fixing
Change-type: patch
2020-02-06 17:29:23 +00:00
0f17129c2e v11.25.3 2020-02-06 19:25:49 +02:00
9005affe64 Merge pull request #1603 from balena-io/1591-1596-mmmagic-linux
Fix "could not load any valid magic files"
2020-02-06 17:24:10 +00:00
4502f2a203 Avoid loading 'mmmagic' on Linux (fix "could not load any valid magic files")
Resolves: #1596
Change-type: patch
2020-02-05 23:59:11 +00:00
da3c11533c v11.25.2 2020-02-05 05:14:59 +02:00
6acff945ef Merge pull request #1586 from balena-io/disable-debug-mode-with-zero-or-empty
Debug mode can now be disabled with DEBUG=0 env var
2020-02-05 12:13:22 +09:00
b3948d538c Debug mode can now be disabled with DEBUG=0 env var
Added assignment to `process.env.DEBUG` if `process.env.DEBUG` is negative string to `lib/app.ts` and `automation/run.ts` entrypoints

Resolves: #1502
Change-type: patch
Signed-off-by: Thomas Manning <thomasm@balena.io>
2020-02-05 11:17:12 +09:00
f53a69feb1 v11.25.1 2020-02-04 12:33:13 +02:00
405b92114d Merge pull request #1590 from balena-io/remove-unnecessary-code
Remove unnecessary code now that typescript understands `process.exit`
2020-02-04 10:31:23 +00:00
27e1f3f7d7 Remove unnecessary code now that typescript understands process.exit
Change-type: patch
2020-02-03 13:06:23 +00:00
1417875110 v11.25.0 2020-02-03 10:23:28 +02:00
f58a49d6c3 Merge pull request #1583 from balena-io/1273-convert-crlf-on-push
Convert CRLF on push
2020-02-03 09:21:53 +01:00
f9743b269a Add more tests for push/build/deploy commands (--convert-eol)
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-02-02 20:16:57 +00:00
0f5f65e0d3 Add more tests for push/build/deploy commands (--dockerfile)
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-02-02 20:16:43 +00:00
58e7880f1d Add support for auto-conversion of CRLF line endings.
Applies to commands:
 balena push
 balena build
 balena deploy --build

Change-type: minor
Resolves: #1273
Signed-off-by: Scott Lowe <scott@balena.io>
2020-01-31 16:27:22 +01:00
041823189f Add support for deferred log messages.
eg. so that info can be output at the end of the process.

Change-type: patch
Connects-to: #1273
Signed-off-by: Scott Lowe <scott@balena.io>
2020-01-31 11:26:54 +01:00
38194e6175 v11.24.0 2020-01-31 01:42:39 +02:00
c04e9665ad Merge pull request #1582 from balena-io/1579-global-agent-proxy
Fix proxy support and add proxy exclusion feature (Node.js >= 10.16.0)
2020-01-30 23:41:06 +00:00
1e37c97ffb Fix proxy support and add proxy exclusion feature (Node.js >= 10.16.0 only)
See README for more details on proxy configuration and Node.js compatibility.

Resolves: #1579
Resolves: #1335
Connects-to: #1580
Change-type: minor
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-27 12:11:11 +00:00
913f09924a Update Github's templates for new CLI pull requests and issues
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-27 12:11:11 +00:00
ceb47e9969 v11.23.0 2020-01-27 13:12:47 +02:00
305755549e Merge pull request #1581 from balena-io/update-deps
Update dependencies
2020-01-27 11:11:02 +00:00
77931b314a Update dependencies
Change-type: minor
2020-01-24 23:28:15 +00:00
b38b5b0b61 v11.22.0 2020-01-21 23:45:41 +02:00
5cf407b483 Merge pull request #1576 from balena-io/add-gsm-configuration-action
configure: Allow passing system-connection files to `os configure` command
2020-01-21 21:44:07 +00:00
8f6902f4cb configure: Allow passing system-connection files to 'os configure' command
Allow passing files to `os configure` via `--system-connection` to allow
pre-configuration of network connections, such as cellular/GSM.

Change-type: minor
Connects-to: #957
Connects-to: #1162
Connects-to: #1498
Signed-off-by: Rich Bayliss <rich@balena.io>
2020-01-21 21:05:23 +00:00
751f67e997 v11.21.8 2020-01-21 13:06:33 +02:00
be1a260af6 Merge pull request #1572 from balena-io/add-tests-push-build-deploy
Add test cases for the push, build and deploy commands
2020-01-21 06:04:40 -05:00
9db6961a7e Add catch-uncommitted to balena CI build
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-20 23:25:24 +00:00
b978230f9e Update resin-lint and prettier, and re-prettify
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-20 22:46:32 +00:00
cc5fe60a15 Add tests for push, deploy and build commands
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-20 13:27:52 +00:00
bbea58a9c8 v11.21.7 2020-01-20 13:26:30 +02:00
56e35f6e9f Merge pull request #1558 from balena-io/1445-multicontainer-push-fix
Prevent file ignorer from ignoring Dockerfile, docker-compose.yml
2020-01-20 12:24:36 +01:00
95b5ac1c7f Prevent file ignorer from ignoring Dockerfile (and variants), docker-compose.yml
Change-type: patch
Resolves: #1445
Signed-off-by: Scott Lowe <scott@balena.io>
2020-01-20 11:59:04 +01:00
df3e1f1886 v11.21.6 2020-01-20 05:22:14 +02:00
5d34659991 Merge pull request #1563 from balena-io/1562-scan-windows-bonjour
Add Windows-specific hint to 'balena scan' output
2020-01-19 19:20:25 -08:00
aca794b267 Add Windows-specific hint to 'balena scan' output
An extra Windows-specific message is now appended to the 'Could not find any balenaOS devices' message - if the OS is Windows.

Also updated the INSTALL instructions with details of the dependency on Bonjour.

Change-type: patch
Signed-off-by: Graham McCulloch <graham@balena.io>
2020-01-20 07:21:04 +07:00
cd6072ac73 v11.21.5 2020-01-15 13:37:57 +02:00
bda696ad8c Merge pull request #1569 from balena-io/apps-git_repository-to-slug
Change the balena app action to present the slug instead of the git_repository
2020-01-15 13:36:18 +02:00
ef4ee54a00 Change the balena app action to present the slug instead of the git_repository
The `git_repository` field was replaced in the v5
endpoint with the `slug` field. As a result the CLI atm
never shows the `git_repository` in the printed
visual.


Change-type: patch
2020-01-15 01:53:48 +02:00
a2ca8e8f73 v11.21.4 2020-01-14 22:16:39 +02:00
620a0abf31 Merge pull request #1559 from balena-io/1557-ssh-hangs-windows
Fix join and leave commands on Windows (hanging on stdin and argument escaping)
2020-01-14 15:15:07 -05:00
95561864a6 Fix 'balena join' when the user is not logged in
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-14 19:34:53 +00:00
51adfeaa3b Fix join and leave commands on Windows (hanging on stdin and argument escaping)
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-14 19:34:53 +00:00
76447a2177 v11.21.3 2020-01-14 21:32:33 +02:00
a6153869e5 Merge pull request #1556 from balena-io/1182-push-hangs-windows
Fix 'balena push' hanging on Windows
2020-01-14 14:30:43 -05:00
3466be1992 Increase default mocha test timeout to avoid spurious CI failures
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-14 17:12:17 +00:00
95843dd816 Fix 'balena push' hanging on Windows (CTRL-C was required after the unicorn)
Resolves: #1182
Resolves: #1554
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-14 16:19:09 +00:00
edd755d41c Add hint about the 'jq' utility in the documentation of the --json option
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-14 16:19:09 +00:00
290c06074a Add '.nyc_output' folder to '.gitignore' (test coverage reporting)
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-14 16:19:08 +00:00
dd7d9d1570 v11.21.2 2020-01-14 18:05:30 +02:00
c4829153fc Merge pull request #1561 from balena-io/contributing-npm-install
Update CONTRIBUTING.md regarding npm installation and some common gotchas
2020-01-14 11:03:52 -05:00
615f24edd3 Update CONTRIBUTING.md regarding npm installation and some common gotchas
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2020-01-14 14:47:15 +00:00
a94e6d550e v11.21.1 2020-01-14 02:19:39 +02:00
75044030cf Merge pull request #1555 from balena-io/american-british
meta: Americanize all spellings
2020-01-13 19:18:22 -05:00
046743071d meta: Americanize all spellings
Connects-to: https://github.com/balena-io/docs/issues/1300
Change-type: patch
Signed-off-by: Matthew McGinn <matthew@balena.io>
2020-01-03 13:42:34 -05:00
4e95cb0cca v11.21.0 2019-12-27 14:32:41 +02:00
4666019c84 Merge pull request #1553 from balena-io/467-supported-aliases
Add --verbose and --json options to the 'devices supported' command
2019-12-27 12:31:02 +00:00
323c9191b6 Add --verbose and --json options to the 'devices supported' command
The command was also converted to oclif.

Resolves: #467
Change-type: minor
Signed-off-by: Paulo Castro <paulo@balena.io>
2019-12-27 11:54:27 +00:00
024bf2996b v11.20.2 2019-12-17 16:55:00 +02:00
5210d474a9 Merge pull request #1538 from balena-io/update-livepush
Update livepush to fix windows path issue.
2019-12-17 15:53:06 +01:00
3cce8d822c Update livepush to fix windows path issue.
Change-type: patch
Connects-to: https://github.com/balena-io-modules/livepush/issues/55
Signed-off-by: Scott Lowe <scott@balena.io>
2019-12-17 15:24:34 +01:00
65250e431e v11.20.1 2019-12-13 11:01:49 +02:00
29cc75598f Merge pull request #1533 from balena-io/1530-fix-missing-app-issues
Fix issues with devices associated with inaccessible applications.
2019-12-13 10:00:25 +01:00
33552724a1 Fix issues with devices associated with inaccessible applications.
Change-type: patch
Resolves: #1530
Signed-off-by: Scott Lowe <scott@balena.io>
2019-12-13 09:33:59 +01:00
c88b317143 v11.20.0 2019-12-13 02:54:30 +02:00
658b0a5233 Merge pull request #1529 from balena-io/1153-envs-microservices
Add multicontainer (microservices) support for 'balena env(s)'
2019-12-13 00:52:57 +00:00
7fd436cd91 Add multicontainer (microservices) support for 'balena env rename'
Change-type: minor
Signed-off-by: Paulo Castro <paulo@balena.io>
2019-12-12 13:39:26 +00:00
7c1faa6de0 Add multicontainer (microservices) support for 'balena env rm'
Change-type: minor
Signed-off-by: Paulo Castro <paulo@balena.io>
2019-12-12 13:39:26 +00:00
90e184ea1f Add multicontainer (microservices) support for 'balena env add'
Change-type: minor
Signed-off-by: Paulo Castro <paulo@balena.io>
2019-12-11 17:50:08 +00:00
38920a1c59 Add multicontainer (microservices) support for 'balena envs'
Connects-to: #1153
Change-type: minor
Signed-off-by: Paulo Castro <paulo@balena.io>
2019-12-11 17:50:08 +00:00
df58ac7673 Add balena envs '-j' option to produce JSON output
Change-type: minor
Signed-off-by: Paulo Castro <paulo@balena.io>
2019-12-06 15:51:09 +00:00
630d53311a Add logged-in check for balena 'env' commands
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2019-12-06 15:51:09 +00:00
b1eda160e8 v11.19.1 2019-12-06 17:14:20 +02:00
a63c766f04 Merge pull request #1525 from balena-io/1518-workaround-windows-dns-bug
Introduce workaround for windows dns issue on local `balena push`
2019-12-06 16:12:54 +01:00
53325b7c05 Introduce workaround that fixes windows dns issue on balena push using .local device names.
Improve error handling in deployToDevice so that versionErrors don't mask other errors.

Resolves:#1518
Change-type:patch
Signed-off-by:Scott Lowe <scott@balena.io>
2019-12-06 15:32:08 +01:00
622c510d65 v11.19.0 2019-12-05 16:37:52 +02:00
890bea549f Merge pull request #1527 from balena-io/mock-balena-api
Introduce balena-mock-api module and use in tests.
2019-12-05 15:36:04 +01:00
bb19903826 Update app/create and device/supported tests to use new api-mock.
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2019-12-05 15:10:44 +01:00
33210b896b Introduce balena-api-mock module to simplify api mocking.
Upgrade nock to latest.

Change-type: minor
Signed-off-by: Scott Lowe <scott@balena.io>
2019-12-05 15:10:34 +01:00
c2a0e457c0 v11.18.3 2019-11-21 19:12:40 +02:00
f464597069 Merge pull request #1522 from balena-io/1429-device-move-join
Fix 'balena help join' docs re moving devices between apps on the same server
2019-11-21 17:10:57 +00:00
02dcff5b67 Fix 'balena help join' docs re moving devices between apps on the same server
Resolves: #1429
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2019-11-21 15:31:45 +00:00
2f4539b4d1 Merge pull request #1521 from balena-io/598-git-for-windows-console
Add README note regarding Git for Windows console installation choice
2019-11-21 12:00:08 +00:00
6c3429eb0c Add README note regarding Git for Windows console installation choice
Resolves: #598
Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
2019-11-20 23:38:49 +00:00
284 changed files with 34292 additions and 11995 deletions

15
.gitattributes vendored Normal file
View File

@ -0,0 +1,15 @@
# Set all files to use line feed endings (since we can't match only ones without an extension)
* eol=lf
# And then reset all the files with extensions back to default
*.* -eol
*.sh text eol=lf
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
doc/cli.markdown text eol=lf
# crlf for the eol conversion test files
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf
# Prevent auto merging of the npm-shrinkwrap.json file: see notes in CONTRIBUTING.md
/npm-shrinkwrap.json merge=binary

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @CameronDiver @hedss @pdcastro @srlowe @thgreasi
* @CameronDiver @pdcastro @srlowe @thgreasi

View File

@ -1,16 +1,75 @@
- **balena CLI version:** e.g. 11.2.1 (output of the `"balena version"` command)
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
- **Install method:** npm or zip or executable installer
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
# About this issue tracker
*The balena CLI (Command Line Interface) is a tool used to interact with the balena platform.
This GitHub issue tracker is used for bug reports and feature requests regarding the CLI
tool. General and troubleshooting questions (such as setting up your project to work with a
balenalib base image) are encouraged to be posted to the [balena
forums](https://forums.balena.io), which are monitored by balena's support team and where the
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!*
---
*Please keep in mind that we try to use the issue tracker of this repository for specific bug
reports & CLI feature requests. General & troubleshooting questions are encouraged to be posted to
the [balena forums](https://forums.balena.io), which are monitored by balena's support team and
where the community can both contribute and benefit from the answers.*
# Expected Behavior
*Before submitting this issue please check that this issue is not a duplicate. If there is another
issue describing the same problem or feature please add your information to the existing issue's
comments.*
Please describe what you were expecting to happen. If applicable, please add links to
documentation you were following, or to projects that you were trying to push/build.
# Actual Behavior
Please describe what actually happened instead:
* Quoting logs and error message is useful. If possible, quote the **full** output of the
CLI, not just the error message.
* Please quote the **full command line** too. Sometimes users report that they were
"pushing" or "building" a project, but there are several ways to do so and several
possible "targets" such as balenaCloud, openBalena, local balenaOS device, etc.
Examples:
```
balena push myApp
balena push 192.168.0.12
balena deploy myApp
balena deploy myApp --build
balena build . -a myApp
balena build . -A armv7hf -d raspberrypi3
```
Each of the above command lines executes different code behind the scenes, so quoting the
full command line is very helpful.
Running the CLI in debug mode (`--debug` flag or `DEBUG=1` environment variable) may reveal
additional information. The `--logs` option reveals additional information for the commands:
```
balena build . --logs
balena deploy myApp --build --logs
```
# Steps to Reproduce the Problem
This is the most important and helpful part of a bug report. If we cannot reproduce the
problem, it is difficult to tell what the fix should be, or whether code changes have
fixed it.
1.
1.
1.
# Specifications
- **balena CLI version:** e.g. 1.2.3 (output of the `"balena version -a"` command)
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
- **Install method:** npm or zip package or executable installer
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
# Additional References
If applicable, please add additional links to GitHub projects, forums.balena.io threads,
gist.github.com, Google Drive attachments, etc.

View File

@ -1,11 +1,26 @@
<!-- You can remove tags that do not apply. -->
Resolves: # <!-- Refer an issue of this repository that this PR fixes -->
See: <url> <!-- Refer to any external resource, like a PR, document or discussion -->
Depends-on: <url> <!-- This change depends on a PR to get merged/deployed first -->
Change-type: major|minor|patch <!-- The change type of this PR -->
Resolves: # <!-- Refer an issue of this repository that this PR fixes -->
Change-type: major|minor|patch <!-- See https://semver.org/ -->
Depends-on: <url> <!-- This change depends on a PR to get merged/deployed first -->
See: <url> <!-- Refer to any external resource, like a PR, document or discussion -->
---
##### Contributor checklist
<!-- For completed items, change [ ] to [x]. -->
- [ ] Introduces security considerations
- [ ] Affects the development, build or deployment processes of the component
Please check the CONTRIBUTING.md file for relevant information and some
guidance. Keep in mind that the CLI is a cross-platform application that runs
on Windows, macOS and Linux. Tests will be automatically run by balena CI on
all three operating systems, but this will only help if you have added test
code that exercises the modified or added feature code.
Note that each commit message (currently only the first line) will be
automatically copied to the CHANGELOG.md file, so try writing it in a way
that describes the feature or fix for CLI users.
If there isn't a linked issue or if the linked issue doesn't quite match the
PR, please add a PR description to explain its purpose or the features that it
implements. Adding PR comments to blocks of code that aren't self explanatory
usually helps with the review process.
If the PR introduces security considerations or affects the development, build
or release process, please be sure to highlight this in the PR description.
Thank you very much for your contribution!

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# node-waf configuration
.lock-wscript

View File

@ -1,5 +1,2 @@
coffee_script:
config_file: coffeelint.json
javascript:
enabled: false

View File

@ -5,31 +5,26 @@ npm:
os: alpine
architecture: x86_64
node_versions:
- "8"
- "10"
- name: linux
os: alpine
architecture: x86
node_versions:
- "8"
- "10"
- name: darwin
os: macos
architecture: x86_64
node_versions:
- "8"
- "10"
- name: windows
os: windows
architecture: x86_64
node_versions:
- "8"
- "10"
- name: windows
os: windows
architecture: x86
node_versions:
- "8"
- "10"
docker:

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,854 @@ 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.0.0 - 2020-06-16
* v12 RELEASE NOTES: see https://git.io/Jf7hz [Paulo Castro]
* Update 'balena-lint' and apply new prettier rules [Paulo Castro]
* Convert 'logs' command to async/await and add tests [Paulo Castro]
* Add tests for standalone executable via proxy server [Paulo Castro]
* Update 'global-agent' (fix proxy server issues with unauthenticated setup) [Paulo Castro]
* Update 'balena-sdk' from v12 to v13 and update code and tests as needed [Paulo Castro]
* Update 'pkg' dependency (improve support for Node v14) [Paulo Castro]
* Turn v12 feature switch on [Paulo Castro]
* Update minimum Node.js requirement from v8 to v10 [Paulo Castro]
## 11.36.0 - 2020-06-11
* balena device: Add the mac_address field [Thodoris Greasidis]
## 11.35.21 - 2020-06-11
* Allow setting the initialDeviceName [Rich Bayliss]
## 11.35.20 - 2020-06-10
* Restrict error handler typing [Scott Lowe]
## 11.35.19 - 2020-06-09
* Fix handling of BalenaExpiredToken error [Scott Lowe]
## 11.35.18 - 2020-06-05
* v12 preparations: Add feature switch for default eol-converson [Scott Lowe]
* v12 preparations: Fix dockerignore tests on Windows [Paulo Castro]
## 11.35.17 - 2020-06-02
* Convert 'balena device public-url' commands to oclif [Scott Lowe]
## 11.35.16 - 2020-06-02
* v12 preparations: Add feature switch for build/deploy `--logs` option [Paulo Castro]
## 11.35.15 - 2020-05-29
* v12 preparations: Add feature switch for project directory validation [Paulo Castro]
* v12 preparations: Add feature switch for 'balena apps --verbose' [Paulo Castro]
* v12 preparations: Add feature switch for 'devices supported' default columns [Paulo Castro]
* v12 preparations: Amend test cases for '--nogitignore' option [Paulo Castro]
## 11.35.14 - 2020-05-29
* v12 preparations: Add feature switch for 'envs --all' [Scott Lowe]
## 11.35.13 - 2020-05-29
* v12 preparations: Add feature switch to remove id from 'tags' output [Scott Lowe]
## 11.35.12 - 2020-05-29
* v12 preparations: Add feature switch for '--nogitignore' [Paulo Castro]
## 11.35.11 - 2020-05-28
* Convert `tags`, `tag set`, `tag rm` to oclif. [Scott Lowe]
## 11.35.10 - 2020-05-27
* v12 preparations: Add version switch, update login message. [Scott Lowe]
## 11.35.9 - 2020-05-25
* balena deploy: Fix "access denied" pushing images to registry [Paulo Castro]
## 11.35.8 - 2020-05-25
* Fix lazy loading in utils/compose [Pagan Gazzard]
## 11.35.7 - 2020-05-22
* Replace windows dns workaround with single lookup [Scott Lowe]
## 11.35.6 - 2020-05-21
* Convert selected functions to Typescript and async/await (compose.js) [Paulo Castro]
* Add tests for 'balena deploy' [Paulo Castro]
## 11.35.5 - 2020-05-21
* Fix caching by preserving all file stats when pushing to device or cloud [Cameron Diver]
## 11.35.4 - 2020-05-19
* Add unit tests for errors module [Scott Lowe]
## 11.35.3 - 2020-05-18
* Update typescript to 3.9 [Pagan Gazzard]
## 11.35.2 - 2020-05-16
* Fix 'balena login' web authorization hanging with Google Chrome [Paulo Castro]
* Update web page wording for 'balena login' web authorization [Paulo Castro]
* Update `balena preload` help message (clarify accepted image formats) [Paulo Castro]
* Update pre-commit script error message (automation/check-doc.js) [Paulo Castro]
## 11.35.1 - 2020-05-14
* Update GitHub templates for new issues and pull requests [Paulo Castro]
## 11.35.0 - 2020-05-14
* balena apps: add --verbose option to list application slugs (full app name) [Paulo Castro]
* balena app create: fix application existence check [Paulo Castro]
## 11.34.0 - 2020-05-13
* push/build/deploy: add --nogitignore option and update dockerignore filter library [Paulo Castro]
## 11.33.4 - 2020-05-12
* Re-create standalone zip package (release asset) for Windows [Paulo Castro]
## 11.33.3 - 2020-05-12
* Fix usage of livepush v3 features [Cameron Diver]
## 11.33.2 - 2020-05-11
* Fix 'balena app' (rm, restart, info) with numeric app IDs [Paulo Castro]
## 11.33.1 - 2020-05-11
* Update resin-multibuild [Scott Lowe]
## 11.33.0 - 2020-05-11
* Add a deprecation policy [Thodoris Greasidis]
## 11.32.15 - 2020-05-06
* Improve presentation of errors, help [Scott Lowe]
## 11.32.14 - 2020-05-04
* Disable oclif's ts-node registering when running against built code [Pagan Gazzard]
## 11.32.13 - 2020-05-04
* Convert `balena api-key generate` to oclif [Scott Lowe]
## 11.32.12 - 2020-05-04
* Configure the sentry command scope earlier [Pagan Gazzard]
## 11.32.11 - 2020-05-01
* Avoid unnecessary api calls in `balena build` and `balena deploy` [Pagan Gazzard]
## 11.32.10 - 2020-05-01
* Refactor: move error related functions into error module [Scott Lowe]
* Refactor: use checkLoggedIn() instead of exitIfNotLoggedIn() [Scott Lowe]
## 11.32.9 - 2020-05-01
* Convert qemu.js to typescript [Pagan Gazzard]
## 11.32.8 - 2020-05-01
* Enforce lazy loading via tslint import-blacklist [Pagan Gazzard]
## 11.32.7 - 2020-05-01
* Convert app commands to oclif [Scott Lowe]
## 11.32.6 - 2020-05-01
* Improve oclif missing argument/flag errors [Scott Lowe]
## 11.32.5 - 2020-05-01
* Modify oclif help to match balena conventions [Scott Lowe]
## 11.32.4 - 2020-04-30
* Convert gulpfile.coffee to javascript [Pagan Gazzard]
* Convert lib/app-capitano.coffee to javascript [Pagan Gazzard]
## 11.32.3 - 2020-04-30
* Convert lib/actions/index.coffee to typescript [Pagan Gazzard]
## 11.32.2 - 2020-04-30
* Convert lib/utils/deploy.coffee to javascript [Pagan Gazzard]
* Convert lib/actions/build.coffee to javascript [Pagan Gazzard]
## 11.32.1 - 2020-04-30
* Only notify of an update if the new version is actually newer [Pagan Gazzard]
## 11.32.0 - 2020-04-30
* Integrate livepush v3 and live directives [Scott Lowe]
## 11.31.28 - 2020-04-30
* Convert lib/utils/deploy-legacy.coffee to javascript [Pagan Gazzard]
## 11.31.27 - 2020-04-30
* Convert lib/actions/help.coffee to javascript [Pagan Gazzard]
## 11.31.26 - 2020-04-30
* balena build/deploy: Update QEMU version to support newer balenalib images [Paulo Castro]
## 11.31.25 - 2020-04-30
* Add support for global --debug flag [Scott Lowe]
## 11.31.24 - 2020-04-29
* balena deploy: Fix "TypeError: images.push is not iterable" [Paulo Castro]
## 11.31.23 - 2020-04-28
* Fix unhandled promise rejection when using `balena deploy` [Pagan Gazzard]
## 11.31.22 - 2020-04-25
* Convert lib/actions/device.coffee to javascript [Pagan Gazzard]
## 11.31.21 - 2020-04-25
* Install types for modules used in javascript to improve type checking [Pagan Gazzard]
## 11.31.20 - 2020-04-24
* Convert lib/actions/preload.coffee to javascript [Pagan Gazzard]
## 11.31.19 - 2020-04-24
* Convert lib/actions/config.coffee to javascript [Pagan Gazzard]
## 11.31.18 - 2020-04-24
* Convert lib/utils/compose.coffee to javascript [Pagan Gazzard]
## 11.31.17 - 2020-04-24
* Convert lib/utils/docker-coffee.coffee to javascript [Pagan Gazzard]
## 11.31.16 - 2020-04-24
* Convert lib/actions/os.coffee to javascript [Pagan Gazzard]
## 11.31.15 - 2020-04-24
* Convert lib/utils/qemu.coffee to javascript [Pagan Gazzard]
## 11.31.14 - 2020-04-23
* device os-update: Refactor to use the overall_progress field [Thodoris Greasidis]
## 11.31.13 - 2020-04-23
* Remove unnecessary files [Pagan Gazzard]
## 11.31.12 - 2020-04-23
* Convert lib/actions/local/index.coffee to typescript [Pagan Gazzard]
* Convert lib/actions/local/configure.coffee to javascript [Pagan Gazzard]
* Convert lib/utils/tty to typescript [Pagan Gazzard]
## 11.31.11 - 2020-04-22
* Avoid patch-package warning with 'npm install -g --production' [Paulo Castro]
## 11.31.10 - 2020-04-22
* Convert command `scan` to TypeScript, migrate to oclif [Scott Lowe]
## 11.31.9 - 2020-04-22
* Update patch-package (fix remaining source of seemingly random ENOENT error) [Paulo Castro]
## 11.31.8 - 2020-04-22
* Update to balena-release [Pagan Gazzard]
* Update dependencies to pick up performance improvements [Pagan Gazzard]
## 11.31.7 - 2020-04-21
* Update codeowners [Scott Lowe]
## 11.31.6 - 2020-04-21
* Add test coverage for validation module [Scott Lowe]
## 11.31.5 - 2020-04-20
* convert commands `key`, `keys`, `key add`, `key rm` to oclif. [Scott Lowe]
## 11.31.4 - 2020-04-18
* Review CONTRIBUTING.md and add 'instanceof' usage advice [Paulo Castro]
* Review 'instanceof' usage with classes of external packages [Paulo Castro]
* Unpin balena-sdk (bump balena-sdk to v12.33.0) [Paulo Castro]
## 11.31.3 - 2020-04-16
* Fix balena ssh "Application not found" (pin balena-sdk to v12.30.0) [Paulo Castro]
## 11.31.2 - 2020-04-15
* Fix seemingly random ENOENT error (update 'is-installed-globally' dependency) [Paulo Castro]
## 11.31.1 - 2020-04-15
* improve input validation for `key`, `key rm` [Scott Lowe]
## 11.31.0 - 2020-04-15
* device os-update: allow host OS upgrade with development balenaOS images [Scott Lowe]
## 11.30.17 - 2020-04-09
* Convert commands join, leave to oclif. [Scott Lowe]
## 11.30.16 - 2020-04-07
* Minor grammar fix in balena ssh documentation [Hugh Brown]
## 11.30.15 - 2020-04-03
* Convert `internal scandevices`, `internal osinit` to typescript & oclif [Scott Lowe]
## 11.30.14 - 2020-04-03
* Updated dependencies (vulnerability advisory CVE-2019-20149) [Paulo Castro]
## 11.30.13 - 2020-04-02
* Fix project directory validation for 'balena deploy' with pre-built image [Paulo Castro]
## 11.30.12 - 2020-04-01
* Remove unused code from balena note [Scott Lowe]
## 11.30.11 - 2020-04-01
* Check logged in for `balena build` if application specified Correct eroneous -f flag in `balena build` help [Scott Lowe]
## 11.30.10 - 2020-03-31
* Add '-t' option to 'balena ssh' to bypass TTY autodetection (force allocation) [Paulo Castro]
* Handle ssh process exit codes [Paulo Castro]
## 11.30.9 - 2020-03-31
* Convert lib/actions/local/common.coffee to javascript [Pagan Gazzard]
## 11.30.8 - 2020-03-30
* Update README regarding proxy server support [Paulo Castro]
* Fix "the input device is not a TTY" when piping to 'balena ssh' (local device) [Paulo Castro]
* Fix 'balena ssh' on MSYS Windows shell ("unexpected end of file") [Paulo Castro]
* Delete unused code (ssh.coffee) [Paulo Castro]
## 11.30.7 - 2020-03-30
* Convert command `note` to oclif Add oclif support for piped input [Scott Lowe]
* Convert command `settings` to oclif [Scott Lowe]
## 11.30.6 - 2020-03-26
* Clarify `balena device os-update` help re balenaCloud [Scott Lowe]
## 11.30.5 - 2020-03-25
* Use balena-lint for javascript linting and add javascript type-checking [Pagan Gazzard]
## 11.30.4 - 2020-03-24
* Deduplicate `balenaUrl` fetching in events [Pagan Gazzard]
## 11.30.3 - 2020-03-24
* Preserve symlinks for the sake of the balenaCI worker [Pagan Gazzard]
* Add type checking for tests [Pagan Gazzard]
## 11.30.2 - 2020-03-24
* Add support for authentication checking to oclif [Scott Lowe]
## 11.30.1 - 2020-03-19
* Add support for `root` property on oclif commands [Scott Lowe]
## 11.30.0 - 2020-03-19
* Add support for primary/secondary oclif commands [Scott Lowe]
## 11.29.5 - 2020-03-18
* INSTALL.md: emphasize the standalone zip package recommendation for WSL [Paulo Castro]
## 11.29.4 - 2020-03-16
* Switch to native number check [Pagan Gazzard]
* Switch to native string check [Pagan Gazzard]
* Switch to native `Array.isArray` instead of aliases [Pagan Gazzard]
## 11.29.3 - 2020-03-13
* Remove unused typings [Pagan Gazzard]
## 11.29.2 - 2020-03-12
* Fix opn patch (npm installation warning) [Paulo Castro]
## 11.29.1 - 2020-03-12
* Fix `balena local flash` [Pagan Gazzard]
## 11.29.0 - 2020-03-12
<details>
<summary> Update dependencies [Pagan Gazzard] </summary>
> ### balena-sdk-12.29.1 - 2020-03-09
>
> * tests: Improve the dependent application test case [Thodoris Greasidis]
> ### balena-sdk-12.29.0 - 2020-03-09
>
> * typings: Add the contract field to the Image [Thodoris Greasidis]
> * typings: Add is_of__actor on the ApiKey [Thodoris Greasidis]
> * typings: Add `is_public` to the application model [Thodoris Greasidis]
> ### balena-sdk-12.28.2 - 2020-03-06
>
> * Unify the way that the models get exported [Thodoris Greasidis]
> ### balena-sdk-12.28.1 - 2020-03-05
>
> * Convert OS model to typescript [Stevche Radevski]
> ### balena-sdk-12.28.0 - 2020-03-04
>
> * typings: Add DeviceType logoUrl property [Thodoris Greasidis]
> ### balena-sdk-12.27.1 - 2020-03-04
>
> * Update dependencies [Pagan Gazzard]
> ### balena-sdk-12.27.0 - 2020-03-03
>
> * Add missing deviceType typings to ImgConfigOptions [Stevche Radevski]
</details>
## 11.28.17 - 2020-03-12
* Make windows installer remove old files before installation. [Scott Lowe]
## 11.28.16 - 2020-03-11
* Update CONTRIBUTING.md regarding ./bin/balena-dev and oclif commands [Paulo Castro]
* Update CONTRIBUTING.md regarding Coffeescript to Typescript conversion [Paulo Castro]
* Prevent auto merge of npm-shrinkwrap.json and explain it in CONTRIBUTING.md [Paulo Castro]
* Add test case for `build --emulated` [Paulo Castro]
## 11.28.15 - 2020-03-11
* Fix 'balena login' web auth on Linux standalone zip install (xdg-open ENOENT) [Paulo Castro]
## 11.28.14 - 2020-03-10
* Avoid Sentry reporting of selected common "expected" errors [Paulo Castro]
* Fix occasional "CLI prints 'null' and exits" (replace old Raven/Sentry SDK) [Paulo Castro]
* Don't send the full command line to Sentry.io [Paulo Castro]
* Fix occasionally missed command tracking request (oclif commands) [Paulo Castro]
## 11.28.13 - 2020-03-06
* Improve the UX by only printing effective file changes in livepush [Cameron Diver]
## 11.28.12 - 2020-03-06
* Fix `build --emulated` on Linux ("exec format error") [Paulo Castro]
## 11.28.11 - 2020-03-02
* Don't ignore BALENARC_NO_PROXY env var if HTTP(S)_PROXY env vars are defined [Paulo Castro]
* Use types for global-agent and global-tunnel-ng [Pagan Gazzard]
* Remove lodash usage in proxy setup [Pagan Gazzard]
* Don't try to setup a proxy agent when there's no proxy configured [Pagan Gazzard]
## 11.28.10 - 2020-03-02
<details>
<summary> Update dependencies [Pagan Gazzard] </summary>
> ### balena-sdk-12.26.7 - 2020-02-29
>
> * Lazy-load the models props [Pagan Gazzard]
> * Lazy-load the sdk template props [Pagan Gazzard]
> ### balena-sdk-12.26.6 - 2020-02-28
>
> * Remove unnecessary lodash/forEach usage [Pagan Gazzard]
> ### balena-sdk-12.26.5 - 2020-02-28
>
> * Convert the billing model to typescript [Stevche Radevski]
> ### balena-sdk-12.26.4 - 2020-02-27
>
> * Convert image model to typescript [Stevche Radevski]
> ### balena-sdk-12.26.3 - 2020-02-26
>
> * Update dtslint to v3.1.0 [Thodoris Greasidis]
> ### balena-sdk-12.26.2 - 2020-02-26
>
> * typings_tests/pine-options: Update to work with TypeScript v3.8 [Thodoris Greasidis]
> * Bump TypeScript version to ^3.8.2, so that's used in tests [Thodoris Greasidis]
> ### balena-sdk-12.26.1 - 2020-02-26
>
> * application: Fix linter warning [Thodoris Greasidis]
> ### balena-sdk-12.26.0 - 2020-02-24
>
> * Add overall_progress typings to device model [Stevche Radevski]
> ### balena-sdk-12.25.1 - 2020-02-21
>
> * Convert service model to typescript [Stevche Radevski]
> ### balena-sdk-12.25.0 - 2020-02-19
>
> * Add device statuses enum to device resource [Stevche Radevski]
> * Add device status enum and typings [Stevche Radevski]
> ### balena-sdk-12.24.4 - 2020-02-17
>
> * Fix a test case name typo for auth.whoami() [Thodoris Greasidis]
> ### balena-sdk-12.24.3 - 2020-02-17
>
> * auth.getEmail: Fix confusing call expression [Thodoris Greasidis]
> ### balena-sdk-12.24.2 - 2020-02-17
>
> * Fix concealing network errors in auth.whoami() and auth.isLoggedIn() [Thodoris Greasidis]
> ### balena-sdk-12.24.1 - 2020-02-15
>
> * .gitignore: `.idea` directory generated by JetBrains IDE [Thomas Manning]
> ### balena-sdk-12.24.0 - 2020-02-14
>
> * Update `application.getDashboardUrl` example with `application.get` call to get application id [Thomas Manning]
> * Added `getDashboardUrl(id)` to application model [Thomas Manning]
> ### balena-sdk-12.23.9 - 2020-02-12
>
> * appveyor: Run node & browser tests in parallel [Thodoris Greasidis]
> ### balena-sdk-12.23.8 - 2020-02-11
>
> * appveyor: Set to test against node 8 [Thodoris Greasidis]
> ### balena-sdk-12.23.7 - 2020-02-07
>
> * Update balena-register-device to 6.0.1 [Pagan Gazzard]
> ### balena-sdk-12.23.6 - 2020-02-07
>
> * Re-enable balenaCI autoRebase [Thodoris Greasidis]
> ### balena-sdk-12.23.5 - 2020-02-07
>
> * Fix the tag tests failing b/c of public apps [Thodoris Greasidis]
> * Fix the dependent app test randomly failing b/c of public apps [Thodoris Greasidis]
> ### balena-sdk-12.23.4 - 2020-02-07
>
> * Fix the build failing on node v12 [Thodoris Greasidis]
> ### balena-sdk-12.23.3 - 2020-02-06
>
> * Disable balenaCI auto rebase [Thodoris Greasidis]
> ### balena-sdk-12.23.2 - 2020-02-06
>
> * Switch to resin-lint for linting [Pagan Gazzard]
> ### balena-sdk-12.23.1 - 2020-02-06
>
> * Remove unused code [Pagan Gazzard]
> ### balena-sdk-12.23.0 - 2020-01-28
>
> * app.getWithDeviceServiceDetails: Add the release commit in the services [Thodoris Greasidis]
> * device.getWithServiceDetails: Remove unused 'id' selection [Thodoris Greasidis]
> ### balena-sdk-12.22.0 - 2020-01-27
>
> * Allow retrieving applications by application case insensitive slug [Thodoris Greasidis]
> ### balena-sdk-12.21.3 - 2020-01-25
>
> * release.createFromUrl: Fix the non tar url error handling [Thodoris Greasidis]
> ### balena-sdk-12.21.2 - 2020-01-24
>
> * Convert some of the tests to TypeScript [Thodoris Greasidis]
> * Properly type billing.downloadInvoice result [Thodoris Greasidis]
> * Properly type os.download result [Thodoris Greasidis]
> * Add typings for balena-request stream [Thodoris Greasidis]
</details>
## 11.28.9 - 2020-02-29
* Switch to object spreading in favor of _.assign [Pagan Gazzard]
## 11.28.8 - 2020-02-28
* Lazy-load chalk [Pagan Gazzard]
## 11.28.7 - 2020-02-28
* Simplify lazy-loading of resin-cli-visuals with a shared function [Pagan Gazzard]
## 11.28.6 - 2020-02-28
* Make use of capitano's promise support to simplify the code [Pagan Gazzard]
## 11.28.5 - 2020-02-27
* Simplify lazy-loading of balena-sdk by utilizing a shared function [Pagan Gazzard]
## 11.28.4 - 2020-02-25
* Fix build/deploy commands with QEMU emulation and alternative Dockerfile name [Paulo Castro]
* Fix CONTRIBUTING markdown [Paulo Castro]
## 11.28.3 - 2020-02-24
* Update type deps [Pagan Gazzard]
## 11.28.2 - 2020-02-21
* Add pre-commit check for cli.markdown updates and coffeelint execution [Paulo Castro]
* Fix 'test:fast' npm script definition [Paulo Castro]
## 11.28.1 - 2020-02-21
* Add a script to automate nested changelogs [Thodoris Greasidis]
## 11.28.0 - 2020-02-18
* Update resin-multibuild and add app and release template vars [Cameron Diver]
## 11.27.0 - 2020-02-17
* Add tests for project directory validation [Paulo Castro]
* Add project directory validation for balena push / build / deploy commands [Paulo Castro]
* Refactor 'balena push' error handling [Paulo Castro]
* Add and refactor tests for push/build/deploy commands (docker-compose) [Paulo Castro]
## 11.26.0 - 2020-02-14
* Add '--cache-from' option to balena build and deploy commands [Paulo Castro]
## 11.25.18 - 2020-02-13
* Fix balena push "Segmentation fault" on Windows (replace 'mmmagic' with 'isBinaryFile') [Paulo Castro]
## 11.25.17 - 2020-02-12
* Convert lib/actions/auth to typescript [Pagan Gazzard]
## 11.25.16 - 2020-02-12
* Convert lib/auth/index to typescript [Pagan Gazzard]
## 11.25.15 - 2020-02-12
* Convert lib/auth/server to typescript [Pagan Gazzard]
## 11.25.14 - 2020-02-11
* Convert lib/actions/keys to typescript [Pagan Gazzard]
## 11.25.13 - 2020-02-10
* Convert lib/actions/notes to typescript [Pagan Gazzard]
## 11.25.12 - 2020-02-10
* Convert lib/actions/app to typescript [Pagan Gazzard]
## 11.25.11 - 2020-02-10
* Convert lib/auth/utils to typescript [Pagan Gazzard]
## 11.25.10 - 2020-02-08
* CI builds: revert patch-package upgrade to fix patch errors [Paulo Castro]
* Node 13 compatibility: upgrade ext2fs module [Scott Lowe]
## 11.25.9 - 2020-02-07
* Add .gitattributes to check out with the correct line-ending on windows [Pagan Gazzard]
## 11.25.8 - 2020-02-07
* Merge resin-lint linting and fixing steps into one [Pagan Gazzard]
* Remove redundant type checking of tests [Pagan Gazzard]
* Remove duplicate type checking of automation code [Pagan Gazzard]
## 11.25.7 - 2020-02-07
* Fix Windows standalone zip installer (missing mmmagic db for CRLF conversion) [Paulo Castro]
## 11.25.6 - 2020-02-06
* Switch from opn to its new name of open [Pagan Gazzard]
## 11.25.5 - 2020-02-06
* Add debug instructions for powershell [Pagan Gazzard]
## 11.25.4 - 2020-02-06
* Use resin-lint for automatic lint fixing [Pagan Gazzard]
## 11.25.3 - 2020-02-06
* Avoid loading 'mmmagic' on Linux (fix "could not load any valid magic files") [Paulo Castro]
## 11.25.2 - 2020-02-05
* Debug mode can now be disabled with DEBUG=0 env var Added assignment to `process.env.DEBUG` if `process.env.DEBUG` is negative string to `lib/app.ts` and `automation/run.ts` entrypoints [Thomas Manning]
## 11.25.1 - 2020-02-03
* Remove unnecessary code now that typescript understands `process.exit` [Pagan Gazzard]
## 11.25.0 - 2020-02-02
* Add more tests for push/build/deploy commands (--convert-eol) [Paulo Castro]
* Add more tests for push/build/deploy commands (--dockerfile) [Paulo Castro]
* Add support for auto-conversion of CRLF line endings. Applies to commands: balena push balena build balena deploy --build [Scott Lowe]
* Add support for deferred log messages. eg. so that info can be output at the end of the process. [Scott Lowe]
## 11.24.0 - 2020-01-27
* Fix proxy support and add proxy exclusion feature (Node.js >= 10.16.0 only) [Paulo Castro]
* Update Github's templates for new CLI pull requests and issues [Paulo Castro]
## 11.23.0 - 2020-01-24
* Update dependencies [Pagan Gazzard]
## 11.22.0 - 2020-01-21
* Configure: Allow passing system-connection files to 'os configure' command [Rich Bayliss]
## 11.21.8 - 2020-01-20
* Add `catch-uncommitted` to balena CI build [Paulo Castro]
* Update resin-lint and prettier, and re-prettify [Paulo Castro]
* Add tests for push, deploy and build commands [Paulo Castro]
## 11.21.7 - 2020-01-20
* Prevent file ignorer from ignoring Dockerfile (and variants), docker-compose.yml [Scott Lowe]
## 11.21.6 - 2020-01-20
* Add Windows-specific hint to 'balena scan' output [Graham McCulloch]
## 11.21.5 - 2020-01-14
* Change the balena app action to present the slug instead of the git_repository [Thodoris Greasidis]
## 11.21.4 - 2020-01-14
* Fix 'balena join' when the user is not logged in [Paulo Castro]
* Fix join and leave commands on Windows (hanging on stdin and argument escaping) [Paulo Castro]
## 11.21.3 - 2020-01-14
* Increase default mocha test timeout to avoid spurious CI failures [Paulo Castro]
* Fix 'balena push' hanging on Windows (CTRL-C was required after the unicorn) [Paulo Castro]
* Add hint about the 'jq' utility in the documentation of the --json option [Paulo Castro]
* Add '.nyc_output' folder to '.gitignore' (test coverage reporting) [Paulo Castro]
## 11.21.2 - 2020-01-14
* Update CONTRIBUTING.md regarding npm installation and some common gotchas [Paulo Castro]
## 11.21.1 - 2020-01-13
* Meta: Americanize all spellings [Matthew McGinn]
## 11.21.0 - 2019-12-27
* Add --verbose and --json options to the 'devices supported' command [Paulo Castro]
## 11.20.2 - 2019-12-17
* Update livepush to fix windows path issue. [Scott Lowe]
## 11.20.1 - 2019-12-13
* Fix issues with devices associated with inaccessible applications. [Scott Lowe]
## 11.20.0 - 2019-12-12
* Add multicontainer (microservices) support for 'balena env rename' [Paulo Castro]
* Add multicontainer (microservices) support for 'balena env rm' [Paulo Castro]
* Add multicontainer (microservices) support for 'balena env add' [Paulo Castro]
* Add multicontainer (microservices) support for 'balena envs' [Paulo Castro]
* Add balena envs '-j' option to produce JSON output [Paulo Castro]
* Add logged-in check for balena 'env' commands [Paulo Castro]
## 11.19.1 - 2019-12-06
* Introduce workaround that fixes windows dns issue on `balena push` using .local device names. Improve error handling in deployToDevice so that versionErrors don't mask other errors. [Scott Lowe]
## 11.19.0 - 2019-12-05
* Update app/create and device/supported tests to use new api-mock. [Scott Lowe]
* Introduce balena-api-mock module to simplify api mocking. Upgrade nock to latest. [Scott Lowe]
## 11.18.3 - 2019-11-21
* Fix 'balena help join' docs re moving devices between apps on the same server [Paulo Castro]
* Add README note regarding Git for Windows console installation choice [Paulo Castro]
## 11.18.2 - 2019-11-15
* Use helpers version of `cleanOutput` in tests. Simplify expect semantics in tests. [Scott Lowe]

View File

@ -2,13 +2,47 @@
The balena CLI is an open source project and your contribution is welcome!
After cloning this repository and running `npm install`, the CLI can be built with `npm run build`
and executed with `./bin/balena`. In order to ease development:
* 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`.
* 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`).
In order to ease development:
* `npm run build:fast` skips some of the build steps for interactive testing, or
* `./bin/balena-dev` uses `ts-node/register` and `coffeescript/register` to transpile on the fly.
* `npm run test:source` skips testing the standalone zip packages (which is rather slow)
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
Before opening a PR, please be sure to test your changes with `npm test`.
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is
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
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:
Change from:
```
"oclif": {
"commands": "./build/actions-oclif",
"hooks": {
"prerun": "./build/hooks/prerun/track"
```
To:
```
"oclif": {
"commands": "./lib/actions-oclif",
"hooks": {
"prerun": "./lib/hooks/prerun/track"
```
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.
## Semantic versioning and commit messages
@ -45,21 +79,148 @@ 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) requires [MSYS2](https://www.msys2.org/) to be installed. Other than that, the standard
Command Prompt or PowerShell can be used.
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.
## TypeScript vs CoffeeScript, and Capitano vs oclif
## Updating the 'npm-shrinkwrap.json' file
The CLI was originally written in [CoffeeScript](https://coffeescript.org), but we decided to
migrate to [TypeScript](https://www.typescriptlang.org/) in order to take advantage of static
typing and formal programming interfaces. The migration is taking place gradually, as part of
maintenance work or the implementation of new features.
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.**
Meanwhile, as a text (JSON) file, `git` is capable of merging the `npm-shrinkwrap.json` file during
operations like `rebase`, `cherry-pick` and `pull`. But git's automated merge is not the
recommended way of updating the `npm-shrinkwrap.json` file, because it does not take into account
duplicates or conflicts in the dependency tree, or indeed the state of the `package.json` file
(which may have just been merged). In extreme cases, the automated merge may actually result in a
broken installation. For these reasons, automatic merging of the `npm-shrinkwrap.json` was disabled
through the `.gitattributes` file (the "binary merge driver" allows diff'ing but prevents automatic
merging). Operations like `git rebase` may then result in an error like:
```text
$ git rebase master
warning: Cannot merge binary files: npm-shrinkwrap.json (HEAD vs. c34942b9... test)
Auto-merging npm-shrinkwrap.json
CONFLICT (content): Merge conflict in npm-shrinkwrap.json
error: Failed to merge in the changes.
```
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
$ rm -rf node_modules # Linux / Mac
$ rmdir /s node_modules # Windows Command Prompt
$ npm checkout master -- npm-shrinkwrap.json # revert it to the master branch state
$ npm install # "cleanly" update the npm-shrinkwrap.json file
$ git add npm-shrinkwrap.json # add it for committing (solve merge errors)
```
## 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.
Similarly, [Capitano](https://github.com/balena-io/capitano) was originally adopted as the CLI's
framework, but we recently decided to take advantage of [oclif](https://oclif.io/)'s features such
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, and migrating to oclif is a solution). 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 `app.ts` to decide whether to route full parsing
to Capitano or oclif).
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.
## Programming style
`npm run build` also runs [balena-lint](https://www.npmjs.com/package/@balena/lint), which automatically
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
code, so writing some test code is a great idea. Having said that, there are also some common
gotchas to bear in mind:
* Forward slashes ('/') _vs._ backslashes ('\') in file paths. The Node.js
[path.sep](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_sep) variable stores a
platform-specific path separator character: the backslash on Windows and the forward slash on
Linux and macOS. The
[path.join](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_join_paths) function
builds paths using such platform-specific path separator. However:
* Note that Windows (kernel, cmd.exe, PowerShell, many applications) accepts ***both*** forward
slashes and backslashes as path separators (including mixing them in a path string), so code
like `mypath.split(path.sep)` may fail on Windows if `mypath` contains forward slashes. The
[path.parse](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_parse_path) function
understands both forward slashes and backslashes on Windows, and the
[path.normalize](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_normalize_path)
function will _replace_ forward slashes with backslashes.
* In [tar](https://en.wikipedia.org/wiki/Tar_(computing)#File_format) streams sent to the Docker
daemon and to balenaCloud, the forward slash is the only acceptable path separator, regardless
of the OS where the CLI is running. Therefore, `path.sep` and `path.join` should never be used
when handling paths in tar streams! `path.posix.join` may be used instead of `path.join`.
* Avoid using the system shell to execute external commands, for example:
`child_process.exec('ssh "arg1" "arg2"');`
`child_process.spawn('ssh "arg1" "arg2"', { shell: true });`
Besides the usual security concerns of unsanitized strings, another problem is to get argument
escaping right because of the differences between the Windows 'cmd.exe' shell and the Unix
'/bin/sh'. For example, 'cmd.exe' doesn't recognize single quotes like '/bin/sh', and uses the
caret (^) instead of the backslash as the escape character. Bug territory! Most of the time,
it is possible to avoid relying on the shell altogether by providing a Javascript array of
arguments:
`spawn('ssh', ['arg1', 'arg2'], { shell: false});`
To allow for logging and debugging, the [which](https://www.npmjs.com/package/which) package may
be used to get the full path of a command before executing it, without relying on any shell:
`const fullPath = await which('ssh');`
`console.log(fullPath); # 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'`
`spawn(fullPath, ['arg1', 'arg2'], { shell: false });`
* Avoid the `instanceof` operator when testing against classes/types from external packages
(including base classes), because `npm install` may result in multiple versions of the same
package being installed (to satisfy declared dependencies) and a false negative may result when
comparing an object instance from one package version with a class of another package version
(even if the implementations are identical in both packages). For example, once we fixed a bug
where the test:
`error instanceof BalenaApplicationNotFound`
changed from true to false because `npm install` added an additional copy of the `balena-errors`
package to satisfy a minor `balena-sdk` version update:
`$ find node_modules -name balena-errors`
`node_modules/balena-errors`
`node_modules/balena-sdk/node_modules/balena-errors`
In the case of subclasses of `TypedError`, a string comparison may be used instead:
`error.name === 'BalenaApplicationNotFound'`

View File

@ -2,13 +2,13 @@
There are 3 options to choose from to install balena's CLI:
* [Executable Installer](#executable-installer): the easiest method, using the traditional
graphical desktop application installers for Windows and macOS (coming soon for Linux users too).
* [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. Recommended for scripted installation in CI (continuous integration)
environments.
* [NPM Installation](#npm-installation): recommended for developers who may be interested in
integrating the balena CLI in their existing Node.js projects or workflow.
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).
@ -18,17 +18,18 @@ Dependencies](#additional-dependencies).
> 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), the recommendation is to
> install a balena CLI release for Linux rather than Windows, like the Linux standalone zip
> package. An installation with the graphical executable installer for Windows will not run on
> WSL.
> 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-v11.6.0-windows-x64-installer.exe`
`balena-cli-v11.6.0-macOS-x64-installer.pkg`
`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
@ -59,19 +60,28 @@ macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
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-v10.13.6-linux-x64-standalone.zip`
`balena-cli-v10.13.6-macOS-x64-standalone.zip`
`balena-cli-v10.13.6-windows-x64-standalone.zip`
`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)._
> * 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
@ -86,9 +96,9 @@ some additional development tools to be installed first:
* [Node.js](https://nodejs.org/) version 8, 10 or 12
* **Linux, macOS** and **Windows Subsystem for Linux (WSL):**
Installing Node via [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) is recommended.
With some Linux distributions like Ubuntu, users sometimes report permission errors when using
the system's Node installation (i.e. when Node is installed via `apt-get`), hence the
[nvm](https://github.com/creationix/nvm) recommendation. This [sample
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.
* If using **Node v8,** upgrade `npm` to version 6.9.0 or later with `"npm install -g npm"`
@ -141,11 +151,13 @@ especially if you're using a user-managed node install such as [nvm](https://git
([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.
* If you need SSH to work behind a proxy, you will also need to install
[`proxytunnel`](http://proxytunnel.sourceforge.net/) (available as a `proxytunnel` package
for Ubuntu, for example).
Check the [README](https://github.com/balena-io/balena-cli/blob/master/README.md) file
for proxy configuration instructions.
* 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/)
@ -170,6 +182,14 @@ especially if you're using a user-managed node install such as [nvm](https://git
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.
## Configuring SSH keys
The `balena ssh` command requires an SSH key to be added to your balena account. If you had

View File

@ -36,6 +36,11 @@ including:
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
* [MSYS](http://www.mingw.org/wiki/MSYS): select the `msys-rsync` and `msys-openssh` packages too
* [Git for Windows](https://git-for-windows.github.io/)
* During the installation, you will be prompted to choose between _"Use MinTTY"_ and _"Use
Windows' default console window"._ Choose the latter, because of the same [MSYS2
bug](https://github.com/msys2/MINGW-packages/issues/1633) mentioned above (Git for Windows
actually uses MSYS2). For a screenshot, check this
[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
@ -59,19 +64,67 @@ $ balena login
### Proxy support
HTTP(S) proxies can be configured through any of the following methods, in order of preference:
HTTP(S) proxies can be configured through any of the following methods, in precedence order
(from higher to lower):
* Set the `BALENARC_PROXY` environment variable in URL format (with protocol, host, port, and
optionally basic auth).
* Alternatively, use the [balena config file](https://www.npmjs.com/package/balena-settings-client#documentation)
(project-specific or user-level) and set the `proxy` setting. It can be:
* A string in URL format, or
* An object in the [global-tunnel-ng options format](https://www.npmjs.com/package/global-tunnel-ng#options) (which allows more control).
* Alternatively, set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
environment variable (in the same standard URL format).
* The `BALENARC_PROXY` environment variable in URL format, with protocol (`http` or `https`),
host, port and optionally basic auth. Examples:
* `export BALENARC_PROXY='https://bob:secret@proxy.company.com:12345'`
* `export BALENARC_PROXY='http://localhost:8000'`
To get a proxy to work with the `balena ssh` command, check the
[installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
* The `proxy` setting in the [CLI config
file](https://www.npmjs.com/package/balena-settings-client#documentation). It may be:
* A string in URL format, e.g. `proxy: 'http://localhost:8000'`
* An object in the format:
```yaml
proxy:
protocol: 'http'
host: 'proxy.company.com'
port: 12345
proxyAuth: 'bob:secret'
```
* 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 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
> 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
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4
addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` hostnames are excluded
from proxying. Other hostnames that resolve to private IPv4 addresses are **not** excluded by
default, because matching takes place before name resolution.
`localhost` and `127.0.0.1` are always excluded from proxying, regardless of the value of
BALENARC_NO_PROXY.
The format of the `BALENARC_NO_PROXY` environment variable is a comma-separated list of patterns
that are matched against hostnames or IP addresses. For example:
```
export BALENARC_NO_PROXY='*.local,dev*.mycompany.com,192.168.*'
```
Matched patterns are excluded from proxying. Wildcard expressions are documented at
[matcher](https://www.npmjs.com/package/matcher#usage). Matching takes place _before_ name
resolution, so a pattern like `'192.168.*'` will **not** match a hostname that resolves to an IP
address like `192.168.1.2`.
## Command reference documentation
@ -87,6 +140,22 @@ If you come across any problems or would like to get in touch:
* 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/).
## 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
latest v10 release, would remain compatible with the balenaCloud backend for one
year from the date when v11.0.0 is released.
At the end of this period, the older major version is considered deprecated and
some of the functionality that depends on balenaCloud services may stop working
at any time.
Users are encouraged to regularly update the balena CLI to the latest version.
## Contributing (including editing documentation files)
Please have a look at the [CONTRIBUTING.md](./CONTRIBUTING.md) file for some guidance before

View File

@ -18,7 +18,7 @@
import { run as oclifRun } from '@oclif/dev-cli';
import * as archiver from 'archiver';
import * as Bluebird from 'bluebird';
import { execFile, spawn } from 'child_process';
import { execFile } from 'child_process';
import { stripIndent } from 'common-tags';
import * as filehound from 'filehound';
import * as fs from 'fs-extra';
@ -27,15 +27,17 @@ import * as path from 'path';
import { exec as execPkg } from 'pkg';
import * as rimraf from 'rimraf';
import * as semver from 'semver';
import * as shellEscape from 'shell-escape';
import * as util from 'util';
export const ROOT = path.join(__dirname, '..');
// Note: the following 'tslint disable' line was only required to
// satisfy ts-node under Appveyor's MSYS2 on Windows -- oddly specific.
// Maybe something to do with '/' vs '\' in paths in some tslint file.
// tslint:disable-next-line:no-var-requires
export const packageJSON = require(path.join(ROOT, 'package.json'));
import {
getSubprocessStdout,
loadPackageJson,
MSYS2_BASH,
ROOT,
whichSpawn,
} from './utils';
export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version;
const arch = process.arch;
@ -69,34 +71,6 @@ export const finalReleaseAssets: { [platform: string]: string[] } = {
linux: [standaloneZips['linux']],
};
const MSYS2_BASH = 'C:\\msys64\\usr\\bin\\bash.exe';
/**
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
* The given argv arguments are escaped using the 'shell-escape' package,
* so that backslashes in Windows paths, and other bash-special characters,
* are preserved. If argv is not provided, defaults to process.argv, to the
* effect that this current (parent) process is re-executed under MSYS2 bash.
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
* Windows.
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
*/
export async function runUnderMsys(argv?: string[]) {
const newArgv = argv || process.argv;
await new Promise((resolve, reject) => {
const args = ['-lc', shellEscape(newArgv)];
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
child.on('close', code => {
if (code) {
console.log(`runUnderMsys: child process exited with code ${code}`);
reject(code);
} else {
resolve();
}
});
});
}
/**
* Use the 'pkg' module to create a single large executable file with
* the contents of 'node_modules' and the CLI's javascript code.
@ -121,17 +95,18 @@ async function buildPkg() {
await execPkg(args);
const xpaths: Array<[string, string[]]> = [
// [platform, [path, to, file]]
['*', ['opn', 'xdg-open']],
['darwin', ['denymount', 'bin', 'denymount']],
const paths: Array<[string, string[], string[]]> = [
// [platform, [source path], [destination path]]
['*', ['open', 'xdg-open'], ['xdg-open']],
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
];
await Bluebird.map(xpaths, ([platform, xpath]) => {
await Bluebird.map(paths, ([platform, source, dest]) => {
if (platform === '*' || platform === process.platform) {
// eg copy from node_modules/opn/xdg-open to build-bin/xdg-open
// eg copy from node_modules/open/xdg-open to build-bin/xdg-open
return fs.copy(
path.join(ROOT, 'node_modules', ...xpath),
path.join(ROOT, 'build-bin', xpath.pop()!),
path.join(ROOT, 'node_modules', ...source),
path.join(ROOT, 'build-bin', ...dest),
);
}
});
@ -143,7 +118,7 @@ async function buildPkg() {
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
await Bluebird.map(nativeExtensionPaths, extPath =>
await Bluebird.map(nativeExtensionPaths, (extPath) =>
fs.copy(
extPath,
extPath.replace(
@ -183,9 +158,7 @@ async function testPkg() {
}
if (semver.major(process.version) !== pkgNodeMajorVersion) {
throw new Error(
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${
process.version
}"`,
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
);
}
console.log('Success! (standalone package test successful)');
@ -295,7 +268,7 @@ export async function buildOclifInstaller() {
}
for (const dir of dirs) {
console.log(`rimraf(${dir})`);
await Bluebird.fromCallback(cb => rimraf(dir, cb));
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
}
console.log('=======================================================');
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
@ -315,62 +288,25 @@ export async function buildOclifInstaller() {
}
/**
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
* Wrapper around the npm `catch-uncommitted` package in order to run it
* conditionally, only when:
* - A CI env var is set (CI=true), and
* - The OS is not Windows. (`catch-uncommitted` fails on Windows)
*/
export function fixPathForMsys(p: string): string {
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
}
/**
* Run the executable at execPath as a child process, and resolve a promise
* to the executable's stdout output as a string. Reject the promise if
* anything is printed to stderr, or if the child process exits with a
* non-zero exit code.
* @param execPath Executable path
* @param args Command-line argument for the executable
*/
async function getSubprocessStdout(
execPath: string,
args: string[],
): Promise<string> {
const child = spawn(execPath, args);
return new Promise((resolve, reject) => {
let stdout = '';
child.stdout.on('error', reject);
child.stderr.on('error', reject);
child.stdout.on('data', (data: Buffer) => {
try {
stdout = data.toString();
} catch (err) {
reject(err);
}
});
child.stderr.on('data', (data: Buffer) => {
try {
const stderr = data.toString();
// ignore any debug lines, but ensure that we parse
// every line provided to the stderr stream
const lines = _.filter(
stderr.trim().split(/\r?\n/),
line => !line.startsWith('[debug]'),
);
if (lines.length > 0) {
reject(
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
);
}
} catch (err) {
reject(err);
}
});
child.on('exit', (code: number) => {
if (code) {
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
} else {
resolve(stdout);
}
});
});
export async function catchUncommitted(): Promise<void> {
if (process.env.DEBUG) {
console.error(`[debug] CI=${process.env.CI} platform=${process.platform}`);
}
if (
process.env.CI &&
['true', 'yes', '1'].includes(process.env.CI.toLowerCase()) &&
process.platform !== 'win32'
) {
await whichSpawn('npx', [
'catch-uncommitted',
'--catch-no-git',
'--skip-node-versionbot-changes',
'--ignore-space-at-eol',
]);
}
}

View File

@ -16,7 +16,6 @@
*/
import * as path from 'path';
import { MarkdownFileParser } from './utils';
/**
@ -32,11 +31,17 @@ const capitanoDoc = {
categories: [
{
title: 'API keys',
files: ['build/actions/api-key.js'],
files: ['build/actions-oclif/api-key/generate.js'],
},
{
title: 'Application',
files: ['build/actions/app.js'],
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',
],
},
{
title: 'Authentication',
@ -44,7 +49,11 @@ const capitanoDoc = {
},
{
title: 'Device',
files: ['build/actions/device.js'],
files: [
'build/actions/device.js',
'build/actions-oclif/devices/supported.js',
'build/actions-oclif/device/public-url.js',
],
},
{
title: 'Environment Variables',
@ -57,7 +66,11 @@ const capitanoDoc = {
},
{
title: 'Tags',
files: ['build/actions/tags.js'],
files: [
'build/actions-oclif/tags.js',
'build/actions-oclif/tag/rm.js',
'build/actions-oclif/tag/set.js',
],
},
{
title: 'Help and Version',
@ -65,7 +78,12 @@ const capitanoDoc = {
},
{
title: 'Keys',
files: ['build/actions/keys.js'],
files: [
'build/actions-oclif/keys.js',
'build/actions-oclif/key/index.js',
'build/actions-oclif/key/add.js',
'build/actions-oclif/key/rm.js',
],
},
{
title: 'Logs',
@ -74,14 +92,14 @@ const capitanoDoc = {
{
title: 'Network',
files: [
'build/actions/scan.js',
'build/actions-oclif/scan.js',
'build/actions/ssh.js',
'build/actions/tunnel.js',
],
},
{
title: 'Notes',
files: ['build/actions/notes.js'],
files: ['build/actions-oclif/note.js'],
},
{
title: 'OS',
@ -101,7 +119,7 @@ const capitanoDoc = {
},
{
title: 'Settings',
files: ['build/actions/settings.js'],
files: ['build/actions-oclif/settings.js'],
},
{
title: 'Local',
@ -113,7 +131,7 @@ const capitanoDoc = {
},
{
title: 'Platform',
files: ['build/actions/join.js', 'build/actions/leave.js'],
files: ['build/actions-oclif/join.js', 'build/actions-oclif/leave.js'],
},
{
title: 'Utilities',
@ -146,6 +164,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
mdParser.getSectionOfTitle('Installation'),
mdParser.getSectionOfTitle('Getting Started'),
mdParser.getSectionOfTitle('Support, FAQ and troubleshooting'),
mdParser.getSectionOfTitle('Deprecation policy'),
]);
capitanoDoc.introduction = sections.join('\n');
return capitanoDoc;

View File

@ -57,17 +57,20 @@ function importCapitanoCommands(jsFilename: string): CapitanoCommand[] {
const commands: CapitanoCommand[] = [];
if (actions.signature) {
commands.push(_.omit(actions, 'action'));
commands.push(_.omit(actions, 'action') as any);
} else {
for (const actionName of Object.keys(actions)) {
const actionCommand = actions[actionName];
commands.push(_.omit(actionCommand, 'action'));
commands.push(_.omit(actionCommand, 'action') as any);
}
}
return commands;
}
function importOclifCommands(jsFilename: string): OclifCommand[] {
// TODO: Currently oclif commands with no `usage` overridden will cause
// an error when parsed. This should be improved so that `usage` does not have
// to be overridden if not necessary.
const command: OclifCommand = require(path.join(process.cwd(), jsFilename))
.default as OclifCommand;
return [command];

View File

@ -24,7 +24,7 @@ import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
import * as utils from './utils';
function renderCapitanoCommand(command: CapitanoCommand): string[] {
const result = [`## ${ent.encode(command.signature)}`, command.help];
const result = [`## ${ent.encode(command.signature)}`, command.help!];
if (!_.isEmpty(command.options)) {
result.push('### Options');
@ -33,6 +33,9 @@ function renderCapitanoCommand(command: CapitanoCommand): string[] {
if (option == null) {
throw new Error(`Undefined option in markdown generation!`);
}
if (option.description == null) {
throw new Error(`Undefined option.description in markdown generation!`);
}
result.push(
`#### ${utils.parseCapitanoOption(option)}`,
option.description,
@ -52,7 +55,7 @@ function renderOclifCommand(command: OclifCommand): string[] {
result.push(description);
if (!_.isEmpty(command.examples)) {
result.push('Examples:', command.examples!.map(v => `\t${v}`).join('\n'));
result.push('Examples:', command.examples!.map((v) => `\t${v}`).join('\n'));
}
if (!_.isEmpty(command.args)) {
@ -103,7 +106,7 @@ function renderToc(categories: Category[]): string[] {
result.push(`- ${category.title}`);
result.push(
category.commands
.map(command => {
.map((command) => {
const signature =
typeof command === 'object'
? command.signature // Capitano
@ -136,10 +139,7 @@ function sortCommands(doc: Document): void {
(cmd: CapitanoCommand | OclifCommand, x: string) =>
typeof cmd === 'object' // Capitano vs oclif command
? cmd.signature.replace(/\W+/g, ' ').includes(x)
: (cmd.usage || '')
.toString()
.replace(/\W+/g, ' ')
.includes(x),
: (cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
),
);
}

View File

@ -18,7 +18,6 @@
import { OptionDefinition } from 'capitano';
import * as ent from 'ent';
import * as fs from 'fs';
import * as _ from 'lodash';
import * as readline from 'readline';
export function getOptionPrefix(signature: string) {
@ -36,11 +35,11 @@ export function getOptionSignature(signature: string) {
export function parseCapitanoOption(option: OptionDefinition): string {
let result = getOptionSignature(option.signature);
if (_.isArray(option.alias)) {
if (Array.isArray(option.alias)) {
for (const alias of option.alias) {
result += `, ${getOptionSignature(alias)}`;
}
} else if (_.isString(option.alias)) {
} else if (typeof option.alias === 'string') {
result += `, ${getOptionSignature(option.alias)}`;
}
@ -94,7 +93,7 @@ export class MarkdownFileParser {
crlfDelay: Infinity,
});
rl.on('line', line => {
rl.on('line', (line) => {
// try to match a line like "## Getting Started", where the number
// of '#' characters is the sectionLevel ('##' -> 2), and the
// sectionTitle is "Getting Started"
@ -127,9 +126,7 @@ export class MarkdownFileParser {
} else {
reject(
new Error(
`Markdown section not found: title="${title}" file="${
this.mdFilePath
}"`,
`Markdown section not found: title="${title}" file="${this.mdFilePath}"`,
),
);
}

84
automation/check-doc.js Normal file
View File

@ -0,0 +1,84 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { stripIndent } = require('common-tags');
const _ = require('lodash');
const { fs } = require('mz');
const path = require('path');
const simplegit = require('simple-git/promise');
const ROOT = path.normalize(path.join(__dirname, '..'));
/**
* Compare the timestamp of cli.markdown with the timestamp of staged files,
* issuing an error if cli.markdown is older.
* If cli.markdown does not require updating and the developer cannot run
* `npm run build` on their laptop, the error message suggests a workaround
* using `touch`.
*/
async function checkBuildTimestamps() {
const git = simplegit(ROOT);
const docFile = path.join(ROOT, 'doc', 'cli.markdown');
const [docStat, gitStatus] = await Promise.all([
fs.stat(docFile),
git.status(),
]);
const stagedFiles = _.uniq([
...gitStatus.created,
...gitStatus.staged,
...gitStatus.renamed.map((o) => o.to),
])
// select only staged files that start with lib/ or typings/
.filter((f) => f.match(/^(lib|typings)[/\\]/))
.map((f) => path.join(ROOT, f));
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
fStats.forEach((fStat, index) => {
if (fStat.mtimeMs > docStat.mtimeMs) {
const fPath = stagedFiles[index];
throw new Error(stripIndent`
--------------------------------------------------------------------------------
ERROR: at least one staged file: "${fPath}"
has a more recent modification timestamp than the documentation file:
"${docFile}"
This probably means that \`npm run build\` or \`npm test\` have not been executed,
and this error can be fixed by doing so. Running \`npm run build\` or \`npm test\`
before commiting is required in order to update the CLI markdown documentation
(in case any command-line options were updated, added or removed) and also to
catch Typescript type check errors sooner and reduce overall waiting time, given
that the CI build/tests are currently rather lengthy.
If you need/wish to bypass this check without running \`npm run build\`, run:
npx touch -am "${docFile}"
and then try again.
--------------------------------------------------------------------------------
`);
}
});
}
async function run() {
try {
await checkBuildTimestamps();
} catch (err) {
console.error(err.message);
process.exitCode = 1;
}
}
run();

View File

@ -7,16 +7,18 @@
* We don't `require('semver')` to allow this script to be run as a npm
* 'preinstall' hook, at which point no dependencies have been installed.
*/
function semverGte(v1, v2) {
let v1Array, v2Array; // number[]
try {
const [, major1, minor1, patch1] = /v?(\d+)\.(\d+).(\d+)/.exec(v1);
const [, major2, minor2, patch2] = /v?(\d+)\.(\d+).(\d+)/.exec(v2);
v1Array = [parseInt(major1), parseInt(minor1), parseInt(patch1)];
v2Array = [parseInt(major2), parseInt(minor2), parseInt(patch2)];
} catch (err) {
throw new Error(`Invalid semver versions: '${v1}' or '${v2}'`);
function parseSemver(version) {
const match = /v?(\d+)\.(\d+).(\d+)/.exec(version);
if (match == null) {
throw new Error(`Invalid semver version: ${version}`);
}
const [, major, minor, patch] = match;
return [parseInt(major, 10), parseInt(minor, 10), parseInt(patch, 10)];
}
function semverGte(v1, v2) {
let v1Array = parseSemver(v1);
let v2Array = parseSemver(v2);
for (let i = 0; i < 3; i++) {
if (v1Array[i] < v2Array[i]) {
return false;
@ -27,29 +29,9 @@ function semverGte(v1, v2) {
return true;
}
function _testSemverGet() {
const assert = require('assert').strict;
assert(semverGte('6.4.1', '6.4.1'));
assert(semverGte('6.4.1', 'v6.4.1'));
assert(semverGte('v6.4.1', '6.4.1'));
assert(semverGte('v6.4.1', 'v6.4.1'));
assert(semverGte('6.4.1', '6.4.0'));
assert(semverGte('6.4.1', '6.3.1'));
assert(semverGte('6.4.1', '5.4.1'));
assert(!semverGte('6.4.1', '6.4.2'));
assert(!semverGte('6.4.1', '6.5.1'));
assert(!semverGte('6.4.1', '7.4.1'));
assert(semverGte('v6.4.1', 'v4.0.0'));
assert(!semverGte('v6.4.1', 'v6.9.0'));
assert(!semverGte('v6.4.1', 'v7.0.0'));
}
function checkNpmVersion() {
const execSync = require('child_process').execSync;
const npmVersion = execSync('npm --version')
.toString()
.trim();
const npmVersion = execSync('npm --version').toString().trim();
const requiredVersion = '6.9.0';
if (!semverGte(npmVersion, requiredVersion)) {
// In case you take issue with the error message below:

View File

@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import * as semver from 'semver';
@ -59,24 +60,17 @@ export async function release() {
}
}
let cachedOctokit: any;
/** Return a cached Octokit instance, creating a new one as needed. */
function getOctokit(): any {
if (cachedOctokit) {
return cachedOctokit;
}
const Octokit = require('@octokit/rest').plugin(
const getOctokit = _.once(function () {
const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin(
require('@octokit/plugin-throttling'),
);
return (cachedOctokit = new Octokit({
return new Octokit({
auth: GITHUB_TOKEN,
throttle: {
onRateLimit: (retryAfter: number, options: any) => {
console.warn(
`Request quota exhausted for request ${options.method} ${
options.url
}`,
`Request quota exhausted for request ${options.method} ${options.url}`,
);
// retries 3 times
if (options.request.retryCount < 3) {
@ -91,8 +85,8 @@ function getOctokit(): any {
);
},
},
}));
}
});
});
/**
* Extract pagination information (current page, total pages, ordinal number)
@ -174,9 +168,7 @@ async function updateGitHubReleaseDescriptions(
);
continue;
}
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${
cliRelease.id
})`;
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${cliRelease.id})`;
if (cliRelease.draft === true) {
console.info(`${skipMsg}: draft release`);
continue;
@ -201,9 +193,7 @@ async function updateGitHubReleaseDescriptions(
}
}
console.info(
`${prefix} updating release "${cliRelease.tag_name}" (${
cliRelease.id
}) old body="${oldBodyPreview}"`,
`${prefix} updating release "${cliRelease.tag_name}" (${cliRelease.id}) old body="${oldBodyPreview}"`,
);
try {
await octokit.repos.updateRelease(updatedRelease);

View File

@ -20,19 +20,24 @@ import * as _ from 'lodash';
import {
buildOclifInstaller,
buildStandaloneZip,
fixPathForMsys,
ROOT,
runUnderMsys,
catchUncommitted,
} from './build-bin';
import {
release,
updateDescriptionOfReleasesAffectedByIssue1359,
} from './deploy-bin';
import { fixPathForMsys, ROOT, runUnderMsys } from './utils';
// DEBUG set to falsy for negative values else is truthy
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
process.env.DEBUG?.toLowerCase(),
)
? ''
: '1';
function exitWithError(error: Error | string): never {
console.error(`Error: ${error}`);
process.exit(1);
throw error; // to please the Typescript compiler
}
/**
@ -54,9 +59,10 @@ export async function run(args?: string[]) {
if (_.isEmpty(args)) {
return exitWithError('missing command-line arguments');
}
const commands: { [cmd: string]: () => void } = {
const commands: { [cmd: string]: () => void | Promise<void> } = {
'build:installer': buildOclifInstaller,
'build:standalone': buildStandaloneZip,
'catch-uncommitted': catchUncommitted,
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
release,
};

View File

@ -1,19 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"strict": true,
"strictPropertyInitialization": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": true,
"resolveJsonModule": true,
"sourceMap": true,
"skipLibCheck": true,
"typeRoots" : [
"../node_modules/@types",
"../typings"
]
}
}

140
automation/update-module.ts Normal file
View File

@ -0,0 +1,140 @@
import { exec } from 'child_process';
import * as semver from 'semver';
const changeTypes = ['major', 'minor', 'patch'] as const;
const validateChangeType = (maybeChangeType: string = 'minor') => {
maybeChangeType = maybeChangeType.toLowerCase();
switch (maybeChangeType) {
case 'patch':
case 'minor':
case 'major':
return maybeChangeType;
default:
console.error(`Invalid change type: '${maybeChangeType}'`);
return process.exit(1);
}
};
const compareSemverChangeType = (oldVersion: string, newVersion: string) => {
const oldSemver = semver.parse(oldVersion)!;
const newSemver = semver.parse(newVersion)!;
for (const changeType of changeTypes) {
if (oldSemver[changeType] !== newSemver[changeType]) {
return changeType;
}
}
};
const run = async (cmd: string) => {
console.info(`Running '${cmd}'`);
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
const p = exec(cmd, { encoding: 'utf8' }, (err, stdout, stderr) => {
if (err) {
reject(err);
return;
}
resolve({ stdout, stderr });
});
p.stdout.pipe(process.stdout);
p.stderr.pipe(process.stderr);
});
};
const getVersion = async (module: string): Promise<string> => {
const { stdout } = await run(`npm ls --json --depth 0 ${module}`);
return JSON.parse(stdout).dependencies[module].version;
};
interface Upstream {
repo: string;
url: string;
module?: string;
}
const getUpstreams = async () => {
const fs = await import('fs');
const repoYaml = fs.readFileSync(__dirname + '/../repo.yml', 'utf8');
const yaml = await import('js-yaml');
const { upstream } = yaml.safeLoad(repoYaml) as {
upstream: Upstream[];
};
return upstream;
};
const printUsage = (upstreams: Upstream[], upstreamName: string) => {
console.error(
`
Usage: npm run update ${upstreamName} $version [$changeType=minor]
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
`,
);
return process.exit(1);
};
// TODO: Drop the wrapper function once we move to TS 3.8,
// which will support top level await.
async function main() {
const upstreams = await getUpstreams();
if (process.argv.length < 3) {
return printUsage(upstreams, '$upstreamName');
}
const upstreamName = process.argv[2];
const upstream = upstreams.find((v) => v.repo === upstreamName);
if (!upstream) {
console.error(
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
.map(({ repo }) => repo)
.join(', ')}`,
);
return process.exit(1);
}
if (process.argv.length < 4) {
printUsage(upstreams, upstreamName);
}
const packageName = upstream.module || upstream.repo;
const oldVersion = await getVersion(packageName);
await run(`npm install ${packageName}@${process.argv[3]}`);
const newVersion = await getVersion(packageName);
if (newVersion === oldVersion) {
console.error(`Already on version '${newVersion}'`);
return process.exit(1);
}
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
const semverChangeType = compareSemverChangeType(oldVersion, newVersion);
const changeType = process.argv[4]
? // if the caller specified a change type, use that one
validateChangeType(process.argv[4])
: // use the same change type as in the dependency, but avoid major bumps
semverChangeType && semverChangeType !== 'major'
? semverChangeType
: 'minor';
console.log(`Using Change-type: ${changeType}`);
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
currentBranch = currentBranch.trim();
console.log(`Currenty on branch: '${currentBranch}'`);
if (currentBranch === 'master') {
await run(`git checkout -b "update-${upstreamName}-${newVersion}"`);
}
await run(`git add package.json npm-shrinkwrap.json`);
await run(
`git commit --message "Update ${upstreamName} to ${newVersion}" --message "Update ${upstreamName} from ${oldVersion} to ${newVersion}" --message "Change-type: ${changeType}"`,
);
}
main();

174
automation/utils.ts Normal file
View File

@ -0,0 +1,174 @@
/**
* @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.
*/
import { spawn } from 'child_process';
import * as _ from 'lodash';
import * as path from 'path';
import * as shellEscape from 'shell-escape';
export const MSYS2_BASH = 'C:\\msys64\\usr\\bin\\bash.exe';
export const ROOT = path.join(__dirname, '..');
export function loadPackageJson() {
return require(path.join(ROOT, 'package.json'));
}
/**
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
*/
export function fixPathForMsys(p: string): string {
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
}
/**
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
* The given argv arguments are escaped using the 'shell-escape' package,
* so that backslashes in Windows paths, and other bash-special characters,
* are preserved. If argv is not provided, defaults to process.argv, to the
* effect that this current (parent) process is re-executed under MSYS2 bash.
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
* Windows.
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
*/
export async function runUnderMsys(argv?: string[]) {
const newArgv = argv || process.argv;
await new Promise((resolve, reject) => {
const args = ['-lc', shellEscape(newArgv)];
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
child.on('close', (code) => {
if (code) {
console.log(`runUnderMsys: child process exited with code ${code}`);
reject(code);
} else {
resolve();
}
});
});
}
/**
* Run the executable at execPath as a child process, and resolve a promise
* to the executable's stdout output as a string. Reject the promise if
* anything is printed to stderr, or if the child process exits with a
* non-zero exit code.
* @param execPath Executable path
* @param args Command-line argument for the executable
*/
export async function getSubprocessStdout(
execPath: string,
args: string[],
): Promise<string> {
const child = spawn(execPath, args);
return new Promise((resolve, reject) => {
let stdout = '';
child.stdout.on('error', reject);
child.stderr.on('error', reject);
child.stdout.on('data', (data: Buffer) => {
try {
stdout = data.toString();
} catch (err) {
reject(err);
}
});
child.stderr.on('data', (data: Buffer) => {
try {
const stderr = data.toString();
// ignore any debug lines, but ensure that we parse
// every line provided to the stderr stream
const lines = _.filter(
stderr.trim().split(/\r?\n/),
(line) => !line.startsWith('[debug]'),
);
if (lines.length > 0) {
reject(
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
);
}
} catch (err) {
reject(err);
}
});
child.on('exit', (code: number) => {
if (code) {
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
} else {
resolve(stdout);
}
});
});
}
/**
* Error handling wrapper around the npm `which` package:
* "Like the unix which utility. Finds the first instance of a specified
* executable in the PATH environment variable. Does not cache the results,
* so hash -r is not needed when the PATH changes."
*
* @param program Basename of a program, for example 'ssh'
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
*/
export async function which(program: string): Promise<string> {
const whichMod = await import('which');
let programPath: string;
try {
programPath = await whichMod(program);
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`'${program}' program not found. Is it installed?`);
}
throw err;
}
return programPath;
}
/**
* Call which(programName) and spawn() with the given arguments. Throw an error
* if the process exit code is not zero.
*/
export async function whichSpawn(
programName: string,
args: string[],
): Promise<void> {
const program = await which(programName);
let error: Error | undefined;
let exitCode: number | undefined;
try {
exitCode = await new Promise<number>((resolve, reject) => {
try {
spawn(program, args, { stdio: 'inherit' })
.on('error', reject)
.on('close', resolve);
} catch (err) {
reject(err);
}
});
} catch (err) {
error = err;
}
if (error || exitCode) {
const msg = [
`${programName} failed with exit code ${exitCode}:`,
`"${program}" [${args}]`,
];
if (error) {
msg.push(`${error}`);
}
throw new Error(msg.join('\n'));
}
}

View File

@ -4,6 +4,9 @@
// operations otherwise, if the pool runs out.
process.env.UV_THREADPOOL_SIZE = '64';
// Disable oclif registering ts-node
process.env.OCLIF_TS_NODE = 0;
// Use fast-boot to cache require lookups, speeding up startup
require('fast-boot2').start({
cacheFile: __dirname + '/.fast-boot.json'

View File

@ -13,7 +13,6 @@ process.env.UV_THREADPOOL_SIZE = '64';
require('fast-boot2').start({
cacheFile: '.fast-boot.json',
});
require('coffeescript/register');
const path = require('path');
const rootDir = path.join(__dirname, '..');

View File

@ -1,127 +0,0 @@
{
"coffeescript_error": {
"level": "error"
},
"arrow_spacing": {
"name": "arrow_spacing",
"level": "error"
},
"no_tabs": {
"name": "no_tabs",
"level": "ignore"
},
"no_trailing_whitespace": {
"name": "no_trailing_whitespace",
"level": "error",
"allowed_in_comments": false,
"allowed_in_empty_lines": false
},
"max_line_length": {
"name": "max_line_length",
"value": 120,
"level": "error",
"limitComments": true
},
"line_endings": {
"name": "line_endings",
"level": "ignore",
"value": "unix"
},
"no_trailing_semicolons": {
"name": "no_trailing_semicolons",
"level": "error"
},
"indentation": {
"name": "indentation",
"value": 1,
"level": "error"
},
"camel_case_classes": {
"name": "camel_case_classes",
"level": "error"
},
"colon_assignment_spacing": {
"name": "colon_assignment_spacing",
"level": "error",
"spacing": {
"left": 0,
"right": 1
}
},
"no_implicit_braces": {
"name": "no_implicit_braces",
"level": "ignore",
"strict": false
},
"no_plusplus": {
"name": "no_plusplus",
"level": "ignore"
},
"no_throwing_strings": {
"name": "no_throwing_strings",
"level": "error"
},
"no_backticks": {
"name": "no_backticks",
"level": "error"
},
"no_implicit_parens": {
"name": "no_implicit_parens",
"strict": false,
"level": "ignore"
},
"no_empty_param_list": {
"name": "no_empty_param_list",
"level": "error"
},
"no_stand_alone_at": {
"name": "no_stand_alone_at",
"level": "ignore"
},
"space_operators": {
"name": "space_operators",
"level": "error"
},
"duplicate_key": {
"name": "duplicate_key",
"level": "error"
},
"empty_constructor_needs_parens": {
"name": "empty_constructor_needs_parens",
"level": "ignore"
},
"cyclomatic_complexity": {
"name": "cyclomatic_complexity",
"value": 10,
"level": "ignore"
},
"newlines_after_classes": {
"name": "newlines_after_classes",
"value": 3,
"level": "ignore"
},
"no_unnecessary_fat_arrows": {
"name": "no_unnecessary_fat_arrows",
"level": "error"
},
"missing_fat_arrows": {
"name": "missing_fat_arrows",
"level": "ignore"
},
"non_empty_constructor_needs_parens": {
"name": "non_empty_constructor_needs_parens",
"level": "ignore"
},
"no_unnecessary_double_quotes": {
"name": "no_unnecessary_double_quotes",
"level": "error"
},
"no_debugger": {
"name": "no_debugger",
"level": "warn"
},
"no_interpolation_in_single_quotes": {
"name": "no_interpolation_in_single_quotes",
"level": "error"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +0,0 @@
path = require('path')
gulp = require('gulp')
coffee = require('gulp-coffee')
inlinesource = require('gulp-inline-source')
shell = require('gulp-shell')
packageJSON = require('./package.json')
OPTIONS =
files:
coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ]
app: 'lib/**/*.coffee'
tests: 'tests/**/*.spec.js'
pages: 'lib/auth/pages/*.ejs'
directories:
build: 'build/'
gulp.task 'pages', ->
gulp.src(OPTIONS.files.pages)
.pipe(inlinesource())
.pipe(gulp.dest('build/auth/pages'))
gulp.task 'coffee', ->
gulp.src(OPTIONS.files.app)
.pipe(coffee(bare: true, header: true))
.pipe(gulp.dest(OPTIONS.directories.build))
gulp.task 'build', gulp.series [
'coffee',
'pages'
]
gulp.task 'watch', gulp.series [ 'build' ], ->
gulp.watch([ OPTIONS.files.coffee ], [ 'build' ])

15
gulpfile.js Normal file
View File

@ -0,0 +1,15 @@
const gulp = require('gulp');
const inlinesource = require('gulp-inline-source');
const OPTIONS = {
files: {
pages: 'lib/auth/pages/*.ejs',
},
};
gulp.task('pages', () =>
gulp
.src(OPTIONS.files.pages)
.pipe(inlinesource())
.pipe(gulp.dest('build/auth/pages')),
);

View File

@ -0,0 +1,86 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
name: string;
}
export default class GenerateCmd extends Command {
public static description = stripIndent`
Generate a new balenaCloud API key.
Generate a new balenaCloud API key for the current user, with the given
name. The key will be logged to the console.
This key can be used to log into the CLI using 'balena login --token <key>',
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
`;
public static examples = ['$ balena api-key generate "Jenkins Key"'];
public static args = [
{
name: 'name',
description: 'the API key name',
required: true,
},
];
public static usage = 'api-key generate <name>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
let key;
try {
key = await getBalenaSdk().models.apiKey.create(params.name);
} catch (e) {
if (e.name === 'BalenaNotLoggedIn') {
throw new ExpectedError(stripIndent`
This command cannot be run when logged in with an API key.
Please login again with 'balena login' and select an alternative method.
`);
} else {
throw e;
}
}
console.log(stripIndent`
Registered api key '${params.name}':
${key}
This key will not be shown again, so please save it now.
`);
}
}

View File

@ -0,0 +1,102 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
interface FlagsDef {
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
export default class AppCreateCmd extends Command {
public static description = stripIndent`
Create an application.
Create a new balena application.
You can specify the application device type with the \`--type\` option.
Otherwise, an interactive dropdown will be shown for you to select from.
You can see a list of supported device types with:
$ balena devices supported
`;
public static examples = [
'$ balena app create MyApp',
'$ balena app create MyApp --type raspberry-pi',
];
public static args = [
{
name: 'name',
description: 'application name',
required: true,
},
];
public static usage = 'app create <name>';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
char: 't',
description:
'application device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
AppCreateCmd,
);
const balena = getBalenaSdk();
const patterns = await import('../../utils/patterns');
// Create application
const deviceType = options.type || (await patterns.selectDeviceType());
let application: import('balena-sdk').Application;
try {
application = await balena.models.application.create({
name: params.name,
deviceType,
});
} catch (err) {
// BalenaRequestError: Request error: Unique key constraint violated
if ((err.message || '').toLowerCase().includes('unique')) {
throw new ExpectedError(
`Error: application "${params.name}" already exists`,
);
}
throw err;
}
console.info(
`Application created: ${application.slug} (${application.device_type}, id ${application.id})`,
);
}
}

View File

@ -0,0 +1,75 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
name: string;
}
export default class AppCmd extends Command {
public static description = stripIndent`
Display information about a single application.
Display detailed information about a single balena application.
`;
public static examples = ['$ balena app MyApp'];
public static args = [
{
name: 'name',
description: 'application name or numeric ID',
required: true,
},
];
public static usage = 'app <name>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppCmd);
const application = await getBalenaSdk().models.application.get(
tryAsInteger(params.name),
);
console.log(
getVisuals().table.vertical(application, [
`$${application.app_name}$`,
'id',
'device_type',
'slug',
'commit',
]),
);
}
}

View File

@ -0,0 +1,62 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } 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`
Restart an application.
Restart all devices that belongs to a certain application.
`;
public static examples = ['$ balena app restart MyApp'];
public static args = [
{
name: 'name',
description: 'application name or numeric ID',
required: true,
},
];
public static usage = 'app restart <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);
await getBalenaSdk().models.application.restart(tryAsInteger(params.name));
}
}

View 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 { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
name: string;
}
export default class AppRmCmd extends Command {
public static description = stripIndent`
Remove an application.
Permanently remove a balena application.
The --yes option may be used to avoid interactive confirmation.
`;
public static examples = [
'$ balena app rm MyApp',
'$ balena app rm MyApp --yes',
];
public static args = [
{
name: 'name',
description: 'application name or numeric ID',
required: true,
},
];
public static usage = 'app rm <name>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
AppRmCmd,
);
const patterns = await import('../../utils/patterns');
// Confirm
await patterns.confirm(
options.yes ?? false,
`Are you sure you want to delete application ${params.name}?`,
);
// Remove
await getBalenaSdk().models.application.remove(tryAsInteger(params.name));
}
}

96
lib/actions-oclif/apps.ts Normal file
View File

@ -0,0 +1,96 @@
/**
* @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 { Application } from 'balena-sdk';
import { stripIndent } from 'common-tags';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
import { isV12 } from '../utils/version';
interface ExtendedApplication extends Application {
device_count?: number;
online_devices?: number;
}
interface FlagsDef {
help: void;
verbose?: boolean;
}
export default class AppsCmd extends Command {
public static description = stripIndent`
List all applications.
list all your balena applications.
For detailed information on a particular application,
use \`balena app <name> instead\`.
`;
public static examples = ['$ balena apps'];
public static usage = 'apps';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
verbose: flags.boolean({
char: 'v',
description: isV12()
? 'No-op since release v12.0.0'
: 'add extra columns in the tabular output (SLUG)',
}),
};
public static authenticated = true;
public static primary = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(AppsCmd);
const _ = await import('lodash');
const balena = getBalenaSdk();
// Get applications
const applications: ExtendedApplication[] = await balena.models.application.getAll(
{
$select: ['id', 'app_name', 'slug', 'device_type'],
$expand: { owns__device: { $select: 'is_online' } },
},
);
// Add extended properties
applications.forEach((application) => {
application.device_count = _.size(application.owns__device);
application.online_devices = _.sumBy(application.owns__device, (d) =>
d.is_online === true ? 1 : 0,
);
});
// Display
console.log(
getVisuals().table.horizontal(applications, [
'id',
'app_name',
options.verbose || isV12() ? 'slug' : '',
'device_type',
'online_devices',
'device_count',
]),
);
}
}

View File

@ -0,0 +1,149 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
enable: boolean;
disable: boolean;
status: boolean;
help?: void;
}
interface ArgsDef {
uuid: string;
// Optional hidden arg to support old command format
legacyUuid?: string;
}
export default class DevicePublicUrlCmd extends Command {
public static description = stripIndent`
Get or manage the public URL for a device.
This command will output the current public URL for the
specified device. It can also enable or disable the URL,
or output the enabled status, using the respective options.
The old command style 'balena device public-url enable <uuid>'
is deprecated, but still supported.
`;
public static examples = [
'$ balena device public-url 23c73a1',
'$ balena device public-url 23c73a1 --enable',
'$ balena device public-url 23c73a1 --disable',
'$ balena device public-url 23c73a1 --status',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to manage',
parse: (dev) => tryAsInteger(dev),
required: true,
},
{
// Optional hidden arg to support old command format
name: 'legacyUuid',
parse: (dev) => tryAsInteger(dev),
hidden: true,
},
];
public static usage = 'device public-url <uuid>';
public static flags: flags.Input<FlagsDef> = {
enable: flags.boolean({
description: 'enable the public URL',
exclusive: ['disable', 'status'],
}),
disable: flags.boolean({
description: 'disable the public URL',
exclusive: ['enable', 'status'],
}),
status: flags.boolean({
description: 'determine if public URL is enabled',
exclusive: ['enable', 'disable'],
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DevicePublicUrlCmd,
);
// Legacy command format support.
// Previously this command used the following format
// (changed due to oclif technicalities):
// `balena device public-url enable|disable|status <uuid>`
if (params.legacyUuid) {
const action = params.uuid;
if (!['enable', 'disable', 'status'].includes(action)) {
throw new ExpectedError(
`Unexpected arguments: ${params.uuid} ${params.legacyUuid}`,
);
}
options.enable = action === 'enable';
options.disable = action === 'disable';
options.status = action === 'status';
params.uuid = params.legacyUuid;
delete params.legacyUuid;
}
const balena = getBalenaSdk();
if (options.enable) {
// Enable public URL
await balena.models.device.enableDeviceUrl(params.uuid);
} else if (options.disable) {
// Disable public URL
await balena.models.device.disableDeviceUrl(params.uuid);
} else if (options.status) {
// Output bool indicating if public URL enabled
const hasUrl = await balena.models.device.hasDeviceUrl(params.uuid);
console.log(hasUrl);
} else {
// Output public URL
try {
const url = await balena.models.device.getDeviceUrl(params.uuid);
console.log(url);
} catch (e) {
if (e.message.includes('Device is not web accessible')) {
throw new ExpectedError(stripIndent`
Public URL is not enabled for this device.
To enable, use:
balena device public-url ${params.uuid} --enable
`);
} else {
throw e;
}
}
}
}
}

View File

@ -0,0 +1,117 @@
/**
* @license
* Copyright 2016-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 { flags } from '@oclif/command';
import * as SDK from 'balena-sdk';
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
import { isV12 } from '../../utils/version';
interface FlagsDef {
discontinued: boolean;
help: void;
json?: boolean;
verbose?: boolean;
}
export default class DevicesSupportedCmd extends Command {
public static description = stripIndent`
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
The --verbose option adds extra columns/fields to the output, including the
"STATE" column whose values are one of 'beta', '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/).
`;
public static examples = [
'$ balena devices supported',
'$ balena devices supported --verbose',
'$ balena devices supported -vj',
];
public static usage = (
'devices supported ' +
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
).trim();
public static flags: flags.Input<FlagsDef> = {
discontinued: flags.boolean({
description: 'include "discontinued" device types',
}),
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
verbose: flags.boolean({
char: 'v',
description:
'add extra columns in the tabular output (ALIASES, ARCH, STATE)',
}),
};
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
let deviceTypes: Array<Partial<SDK.DeviceType>> = await getBalenaSdk()
.models.config.getDeviceTypes()
.map((d) => {
if (d.aliases && d.aliases.length) {
// remove aliases that are equal to the slug
d.aliases = d.aliases.filter((alias: string) => alias !== d.slug);
if (!options.json) {
// stringify the aliases array with commas and spaces
d.aliases = [d.aliases.join(', ')];
}
} else {
// ensure it is always an array (for the benefit of JSON output)
d.aliases = [];
}
return d;
});
if (!options.discontinued) {
deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED');
}
const fields = options.verbose
? ['slug', 'aliases', 'arch', 'state', 'name']
: isV12()
? ['slug', 'aliases', 'arch', 'name']
: ['slug', 'name'];
deviceTypes = _.sortBy(
deviceTypes.map((d) => _.pick(d, fields) as Partial<SDK.DeviceType>),
fields,
);
if (options.json) {
console.log(JSON.stringify(deviceTypes, null, 4));
} else {
const visuals = getVisuals();
const output = await visuals.table.horizontal(deviceTypes, fields);
console.log(output);
}
}
}

View File

@ -15,18 +15,23 @@
* limitations under the License.
*/
import { Command, flags } from '@oclif/command';
import { flags } from '@oclif/command';
import * as BalenaSdk from 'balena-sdk';
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
interface FlagsDef {
application?: string;
device?: string;
application?: string; // application name
device?: string; // device UUID
help: void;
quiet: boolean;
service?: string; // service name
}
interface ArgsDef {
@ -36,22 +41,37 @@ interface ArgsDef {
export default class EnvAddCmd extends Command {
public static description = stripIndent`
Add an environment or config variable to an application or device.
Add an environment or config variable to an application, device or service.
Add an environment or config variable to an application or device, as selected
by the respective command-line options.
Add an environment or config variable to an application, device or service,
as selected by the respective command-line options. Either the --application
or the --device option must be provided, and either may be be used alongside
the --service option to define a service-specific variable. (A service is an
application container in a "microservices" application.) When the --service
option is used in conjunction with the --device option, the service variable
applies to the selected device only. Otherwise, it applies to all devices of
the selected application (i.e., the application's fleet). If the --service
option is omitted, the variable applies to all services.
If VALUE is omitted, the CLI will attempt to use the value of the environment
variable of same name in the CLI process' environment. In this case, a warning
message will be printed. Use \`--quiet\` to suppress it.
Service-specific variables are not currently supported. The given command line
examples variables that apply to all services in an app or device.
'BALENA_' or 'RESIN_' are reserved variable name prefixes used to identify
"configuration variables". Configuration variables control balena platform
features and are treated specially by balenaOS and the balena supervisor
running on devices. They are also stored differently in the balenaCloud API
database. Configuration variables cannot be set for specific services,
therefore the --service option cannot be used when the variable name starts
with a reserved prefix. When defining custom application variables, please
avoid the reserved prefixes.
`;
public static examples = [
'$ balena env add TERM --application MyApp',
'$ balena env add EDITOR vim --application MyApp',
'$ balena env add EDITOR vim --application MyApp --service MyService',
'$ balena env add EDITOR vim --device 7cf02a6',
'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
];
public static args = [
@ -64,7 +84,7 @@ export default class EnvAddCmd extends Command {
name: 'value',
required: false,
description:
"variable value; if omitted, use value from CLI's environment",
"variable value; if omitted, use value from this process' environment",
},
];
@ -73,10 +93,11 @@ export default class EnvAddCmd extends Command {
'env add ' + new CommandHelp({ args: EnvAddCmd.args }).defaultUsage();
public static flags: flags.Input<FlagsDef> = {
application: _.assign({ exclusive: ['device'] }, cf.application),
device: _.assign({ exclusive: ['application'] }, cf.device),
application: { exclusive: ['device'], ...cf.application },
device: { exclusive: ['application'], ...cf.device },
help: cf.help,
quiet: cf.quiet,
service: cf.service,
};
public async run() {
@ -84,15 +105,21 @@ export default class EnvAddCmd extends Command {
EnvAddCmd,
);
const cmd = this;
const balena = (await import('balena-sdk')).fromSharedOptions();
const { exitWithExpectedError } = await import('../../utils/patterns');
if (!options.application && !options.device) {
throw new ExpectedError(
'Either the --application or the --device option must always be used',
);
}
await Command.checkLoggedIn();
if (params.value == null) {
params.value = process.env[params.name];
if (params.value == null) {
throw new Error(
`Environment value not found for variable: ${params.name}`,
`Value not found for environment variable: ${params.name}`,
);
} else if (!options.quiet) {
cmd.warn(
@ -101,12 +128,26 @@ export default class EnvAddCmd extends Command {
}
}
const reservedPrefixes = await getReservedPrefixes();
const isConfigVar = _.some(reservedPrefixes, prefix =>
const balena = getBalenaSdk();
const reservedPrefixes = await getReservedPrefixes(balena);
const isConfigVar = _.some(reservedPrefixes, (prefix) =>
_.startsWith(params.name, prefix),
);
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.service) {
if (isConfigVar) {
throw new ExpectedError(stripIndent`
Configuration variables prefixed with "${reservedPrefixes.join(
'" or "',
)}" cannot be set per service.
Hint: remove the --service option or rename the variable.
`);
}
await setServiceVars(balena, params, options);
return;
}
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.application) {
await balena.models.application[varType].set(
options.application,
@ -119,16 +160,78 @@ export default class EnvAddCmd extends Command {
params.name,
params.value,
);
} else {
exitWithExpectedError('You must specify an application or device');
}
}
}
async function getReservedPrefixes(): Promise<string[]> {
const balena = (await import('balena-sdk')).fromSharedOptions();
const settings = await balena.settings.getAll();
/**
* Add service variables for a device or application.
*/
async function setServiceVars(
sdk: BalenaSdk.BalenaSDK,
params: ArgsDef,
options: FlagsDef,
) {
if (options.application) {
const serviceId = await getServiceIdForApp(
sdk,
options.application,
options.service!,
);
await sdk.models.service.var.set(serviceId, params.name, params.value!);
} else {
const { getDeviceAndAppFromUUID } = await import('../../utils/cloud');
const [device, app] = await getDeviceAndAppFromUUID(
sdk,
options.device!,
['id'],
['app_name'],
);
const serviceId = await getServiceIdForApp(
sdk,
app.app_name,
options.service!,
);
await sdk.models.device.serviceVar.set(
device.id,
serviceId,
params.name,
params.value!,
);
}
}
/**
* Return a sevice ID for the given app name and service name.
*/
async function getServiceIdForApp(
sdk: BalenaSdk.BalenaSDK,
appName: string,
serviceName: string,
): Promise<number> {
let serviceId: number | undefined;
const services = await sdk.models.service.getAllByApplication(appName, {
$filter: { service_name: serviceName },
});
if (!_.isEmpty(services)) {
serviceId = services[0].id;
}
if (serviceId === undefined) {
throw new ExpectedError(
`Cannot find service ${serviceName} for application ${appName}`,
);
}
return serviceId;
}
/**
* Return an array of variable name prefixes like: [ 'RESIN_', 'BALENA_' ].
* These prefixes can be used to identify "configuration variables".
*/
async function getReservedPrefixes(
balena: BalenaSdk.BalenaSDK,
): Promise<string[]> {
const settings = await balena.settings.getAll();
const response = await balena.request.send({
baseUrl: settings.apiUrl,
url: '/config/vars',

View File

@ -14,16 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Command, flags } from '@oclif/command';
import { flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ec from '../../utils/env-common';
import { getBalenaSdk } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
config: boolean;
device: boolean;
service: boolean;
help: void;
}
@ -34,59 +40,57 @@ interface ArgsDef {
export default class EnvRenameCmd extends Command {
public static description = stripIndent`
Change the value of an environment variable for an app or device.
Change the value of a config or env var for an app, device or service.
Change the value of an environment variable for an application or device,
as selected by the '--device' option. The variable is identified by its
database ID, rather than its name. The 'balena envs' command can be used
to list the variable's ID.
Change the value of a configuration or environment variable for an application,
device or service, as selected by command-line options.
Service-specific variables are not currently supported. The following
examples modify variables that apply to all services in an app or device.
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena env rename 376 emacs',
'$ balena env rename 376 emacs --device',
'$ balena env rename 123123 emacs',
'$ balena env rename 234234 emacs --service',
'$ balena env rename 345345 emacs --device',
'$ balena env rename 456456 emacs --device --service',
'$ balena env rename 567567 1 --config',
'$ balena env rename 678678 1 --device --config',
];
public static args: Array<IArg<any>> = [
{
name: 'id',
required: true,
description: 'environment variable numeric database ID',
parse: input => parseInt(input, 10),
description: "variable's numeric database ID",
parse: (input) => parseAsInteger(input, 'id'),
},
{
name: 'value',
required: true,
description:
"variable value; if omitted, use value from CLI's environment",
"variable value; if omitted, use value from this process' environment",
},
];
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
// hardcoded 'env rename' to avoid oclif's 'env:rename' topic syntax
public static usage =
'env rename ' + new CommandHelp({ args: EnvRenameCmd.args }).defaultUsage();
public static flags: flags.Input<FlagsDef> = {
device: flags.boolean({
char: 'd',
description:
'select a device variable instead of an application variable',
}),
config: ec.booleanConfig,
device: ec.booleanDevice,
service: ec.booleanService,
help: cf.help,
};
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
EnvRenameCmd,
);
const balena = (await import('balena-sdk')).fromSharedOptions();
await balena.pine.patch({
resource: options.device
? 'device_environment_variable'
: 'application_environment_variable',
await Command.checkLoggedIn();
await getBalenaSdk().pine.patch({
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
id: params.id,
body: {
value: params.value,

View File

@ -15,14 +15,21 @@
* limitations under the License.
*/
import { Command, flags } from '@oclif/command';
import { flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as ec from '../../utils/env-common';
import { getBalenaSdk } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
config: boolean;
device: boolean;
service: boolean;
yes: boolean;
}
@ -32,88 +39,69 @@ interface ArgsDef {
export default class EnvRmCmd extends Command {
public static description = stripIndent`
Remove an environment variable from an application or device.
Remove a config or env var from an application, device or service.
Remove a configuration or environment variable from an application or device,
as selected by command-line options.
Remove a configuration or environment variable from an application, device
or service, as selected by command-line options.
Note that this command asks for confirmation interactively.
You can avoid this by passing the \`--yes\` boolean option.
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
The --device option selects a device instead of an application.
The --config option selects a config var instead of an env var.
Service-specific variables are not currently supported. The following
examples remove variables that apply to all services in an app or device.
Interactive confirmation is normally asked before the variable is deleted.
The --yes option disables this behavior.
`;
public static examples = [
'$ balena env rm 215',
'$ balena env rm 215 --yes',
'$ balena env rm 215 --config',
'$ balena env rm 215 --device',
'$ balena env rm 215 --device --config',
'$ balena env rm 123123',
'$ balena env rm 234234 --yes',
'$ balena env rm 345345 --config',
'$ balena env rm 456456 --service',
'$ balena env rm 567567 --device',
'$ balena env rm 678678 --device --config',
'$ balena env rm 789789 --device --service --yes',
];
public static args = [
public static args: Array<IArg<any>> = [
{
name: 'id',
required: true,
description: 'environment variable numeric database ID',
description: "variable's numeric database ID",
parse: (input) => parseAsInteger(input, 'id'),
},
];
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
// hardcoded 'env rm' to avoid oclif's 'env:rm' topic syntax
public static usage =
'env rm ' + new CommandHelp({ args: EnvRmCmd.args }).defaultUsage();
public static flags: flags.Input<FlagsDef> = {
device: flags.boolean({
char: 'd',
description:
'Selects a device environment variable instead of an application environment variable',
default: false,
}),
config: flags.boolean({
char: 'c',
description:
'Selects a configuration variable instead of an environment variable',
default: false,
}),
config: ec.booleanConfig,
device: ec.booleanDevice,
service: ec.booleanService,
yes: flags.boolean({
char: 'y',
description: 'Run in non-interactive mode',
description:
'do not prompt for confirmation before deleting the variable',
default: false,
}),
};
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
EnvRmCmd,
);
const balena = (await import('balena-sdk')).fromSharedOptions();
const patterns = await import('../../utils/patterns');
const balena = getBalenaSdk();
const { confirm } = await import('../../utils/patterns');
if (isNaN(params.id) || !Number.isInteger(Number(params.id))) {
patterns.exitWithExpectedError(
'The environment variable id must be an integer',
);
}
await Command.checkLoggedIn();
await patterns.confirm(
options.yes || false,
await confirm(
opt.yes || false,
'Are you sure you want to delete the environment variable?',
undefined,
true,
);
await balena.pine.delete({
resource: options.device
? options.config
? 'device_config_variable'
: 'device_environment_variable'
: options.config
? 'application_config_variable'
: 'application_environment_variable',
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
id: params.id,
});
}

View File

@ -14,82 +14,432 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Command, flags } from '@oclif/command';
import { ApplicationVariable, DeviceVariable } from 'balena-sdk';
import { flags } from '@oclif/command';
import * as SDK from 'balena-sdk';
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
import { CommandHelp } from '../utils/oclif-utils';
import { isV12 } from '../utils/version';
interface FlagsDef {
application?: string;
all?: boolean; // whether to include application-wide, device-wide variables //TODO: REMOVE
application?: string; // application name
config: boolean;
device?: string;
device?: string; // device UUID
json: boolean;
help: void;
service?: string; // service name
verbose: boolean;
}
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
appName?: string | null; // application name
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
interface DeviceServiceEnvironmentVariableInfo
extends SDK.DeviceServiceEnvironmentVariable {
appName?: string; // application name
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
interface ServiceEnvironmentVariableInfo
extends SDK.ServiceEnvironmentVariable {
appName?: string; // application name
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
export default class EnvsCmd extends Command {
public static description = stripIndent`
List the environment or config variables of an app or device.
public static description = isV12()
? stripIndent`
List the environment or config variables of an application, device or service.
List the environment or config variables of an application or device,
as selected by the respective command-line options.
List the environment or configuration variables of an application, device or
service, as selected by the respective command-line options. (A service is
an application container in a "microservices" application.)
The --config option is used to list "configuration variables" that
control balena features.
The results include application-wide (fleet), device-wide (multiple services on
a device) and service-specific variables that apply to the selected application,
device or service. It can be thought of as including "inherited" variables;
for example, a service inherits device-wide variables, and a device inherits
application-wide variables.
Service-specific variables are not currently supported. The following
examples list variables that apply to all services in an app or device.
The printed output may include DEVICE and/or SERVICE columns to distinguish
between application-wide, device-specific and service-specific variables.
An asterisk in these columns indicates that the variable applies to
"all devices" or "all services".
The --config option is used to list "configuration variables" that control
balena platform features, as opposed to custom environment variables defined
by the user. The --config and the --service options are mutually exclusive
because configuration variables cannot be set for specific services.
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. The 'jq' utility may be helpful in shell
scripts (https://stedolan.github.io/jq/manual/). When --json is used, an empty
JSON array ([]) is printed instead of an error message when no variables exist
for the given query. When querying variables for a device, note that the
application name may be null in JSON output (or 'N/A' in tabular output) if the
application linked to the device is no longer accessible by the current user
(for example, in case the current user has been removed from the application
by its owner).
`
: stripIndent`
List the environment or config variables of an application, device or service.
List the environment or configuration variables of an application, device or
service, as selected by the respective command-line options. (A service is
an application container in a "microservices" application.)
The --config option is used to list "configuration variables" that control
balena platform features, as opposed to custom environment variables defined
by the user. The --config and the --service options are mutually exclusive
because configuration variables cannot be set for specific services.
The --all option is used to include application-wide (fleet), device-wide
(multiple services on a device) and service-specific variables that apply to
the selected application, device or service. It can be thought of as including
"inherited" variables: for example, a service inherits device-wide variables,
and a device inherits application-wide variables. Variables are still filtered
out by type with the --config option, such that configuration and non-
configuration variables are never listed together.
When the --all option is used, the printed output may include DEVICE and/or
SERVICE columns to distinguish between application-wide, device-specific and
service-specific variables. An asterisk in these columns indicates that the
variable applies to "all devices" or "all services".
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. The 'jq' utility may be helpful in shell
scripts (https://stedolan.github.io/jq/manual/). When --json is used, an empty
JSON array ([]) is printed instead of an error message when no variables exist
for the given query. When querying variables for a device, note that the
application name may be null in JSON output (or 'N/A' in tabular output) if the
application linked to the device is no longer accessible by the current user
(for example, in case the current user has been removed from the application
by its owner).
`;
public static examples = [
'$ balena envs --application MyApp',
'$ balena envs --application MyApp --config',
'$ balena envs --device 7cf02a6',
];
public static examples = isV12()
? [
'$ balena envs --application MyApp',
'$ balena envs --application MyApp --json',
'$ balena envs --application MyApp --service MyService',
'$ balena envs --application MyApp --service MyService',
'$ balena envs --application MyApp --config',
'$ balena envs --device 7cf02a6',
'$ balena envs --device 7cf02a6 --json',
'$ balena envs --device 7cf02a6 --config --json',
'$ balena envs --device 7cf02a6 --service MyService',
]
: [
'$ balena envs --application MyApp',
'$ balena envs --application MyApp --all --json',
'$ balena envs --application MyApp --service MyService',
'$ balena envs --application MyApp --all --service MyService',
'$ balena envs --application MyApp --config',
'$ balena envs --device 7cf02a6',
'$ balena envs --device 7cf02a6 --all --json',
'$ balena envs --device 7cf02a6 --config --all --json',
'$ balena envs --device 7cf02a6 --all --service MyService',
];
public static usage = (
'envs ' + new CommandHelp({ args: EnvsCmd.args }).defaultUsage()
).trim();
public static flags: flags.Input<FlagsDef> = {
application: _.assign({ exclusive: ['device'] }, cf.application),
...(isV12()
? {
all: flags.boolean({
description: stripIndent`
No-op since balena CLI v12.0.0.`,
hidden: true,
}),
}
: {
all: flags.boolean({
description: stripIndent`
include app-wide, device-wide variables that apply to the selected device or service.
Variables are still filtered out by type with the --config option.`,
}),
}),
application: { exclusive: ['device'], ...cf.application },
config: flags.boolean({
char: 'c',
description: 'show config variables',
description: 'show configuration variables only',
exclusive: ['service'],
}),
device: _.assign({ exclusive: ['application'] }, cf.device),
device: { exclusive: ['application'], ...cf.device },
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
verbose: cf.verbose,
service: { exclusive: ['config'], ...cf.service },
};
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
const balena = (await import('balena-sdk')).fromSharedOptions();
const visuals = await import('resin-cli-visuals');
const { exitWithExpectedError } = await import('../utils/patterns');
const cmd = this;
const variables: EnvironmentVariableInfo[] = [];
let environmentVariables: ApplicationVariable[] | DeviceVariable[];
if (options.application) {
environmentVariables = await balena.models.application[
options.config ? 'configVar' : 'envVar'
].getAllByApplication(options.application);
} else if (options.device) {
environmentVariables = await balena.models.device[
options.config ? 'configVar' : 'envVar'
].getAllByDevice(options.device);
options.all = options.all || isV12();
await Command.checkLoggedIn();
if (!options.application && !options.device) {
throw new ExpectedError('You must specify an application or device');
}
const balena = getBalenaSdk();
const { getDeviceAndMaybeAppFromUUID } = await import('../utils/cloud');
let appName = options.application;
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
if (options.device) {
const [device, app] = await getDeviceAndMaybeAppFromUUID(
balena,
options.device,
['uuid'],
['app_name'],
);
fullUUID = device.uuid;
if (app) {
appName = app.app_name;
}
}
if (appName && options.service) {
await validateServiceName(balena, options.service, appName);
}
if (options.application || options.all) {
variables.push(...(await getAppVars(balena, appName, options)));
}
if (fullUUID) {
variables.push(
...(await getDeviceVars(balena, fullUUID, appName, options)),
);
}
if (!options.json && _.isEmpty(variables)) {
const target =
(options.service ? `service "${options.service}" of ` : '') +
(options.application
? `application "${options.application}"`
: `device "${options.device}"`);
throw new ExpectedError(`No environment variables found for ${target}`);
}
await this.printVariables(variables, options);
}
protected async printVariables(
varArray: EnvironmentVariableInfo[],
options: FlagsDef,
) {
const fields = ['id', 'name', 'value'];
if (options.all) {
// Replace undefined app names with 'N/A' or null
varArray = _.map(varArray, (i: EnvironmentVariableInfo) => {
i.appName = i.appName || (options.json ? null : 'N/A');
return i;
});
fields.push(options.json ? 'appName' : 'appName => APPLICATION');
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
if (!options.config) {
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
}
}
if (options.json) {
this.log(
stringifyVarArray<SDK.EnvironmentVariableBase>(varArray, fields),
);
} else {
return exitWithExpectedError('You must specify an application or device');
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
fields,
),
);
}
}
}
if (_.isEmpty(environmentVariables)) {
return exitWithExpectedError('No environment variables found');
}
cmd.log(
visuals.table.horizontal(environmentVariables, ['id', 'name', 'value']),
async function validateServiceName(
sdk: SDK.BalenaSDK,
serviceName: string,
appName: string,
) {
const services = await sdk.models.service.getAllByApplication(appName, {
$filter: { service_name: serviceName },
});
if (_.isEmpty(services)) {
throw new ExpectedError(
`Service "${serviceName}" not found for application "${appName}"`,
);
}
}
/**
* Fetch application-wide config / env / service vars.
* If options.application is undefined, an attempt is made to obtain the
* application name from the device UUID (options.device). If this attempt
* fails because the device does not belong to any application, an emtpy
* array is returned.
*/
async function getAppVars(
sdk: SDK.BalenaSDK,
appName: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const appVars: EnvironmentVariableInfo[] = [];
if (!appName) {
return appVars;
}
if (options.config || options.all || !options.service) {
const vars = await sdk.models.application[
options.config ? 'configVar' : 'envVar'
].getAllByApplication(appName);
fillInInfoFields(vars, appName);
appVars.push(...vars);
}
if (!options.config && (options.service || options.all)) {
const pineOpts: SDK.PineOptionsFor<SDK.ServiceEnvironmentVariable> = {
$expand: {
service: {},
},
};
if (options.service) {
pineOpts.$filter = {
service: {
service_name: options.service,
},
};
}
const serviceVars = await sdk.models.service.var.getAllByApplication(
appName,
pineOpts,
);
fillInInfoFields(serviceVars, appName);
appVars.push(...serviceVars);
}
return appVars;
}
/**
* Fetch config / env / service vars when the '--device' option is provided.
* Precondition: options.device must be defined.
*/
async function getDeviceVars(
sdk: SDK.BalenaSDK,
fullUUID: string,
appName: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const printedUUID = options.json ? fullUUID : options.device!;
const deviceVars: EnvironmentVariableInfo[] = [];
if (options.config) {
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceConfigVars, appName, printedUUID);
deviceVars.push(...deviceConfigVars);
} else {
if (options.service || options.all) {
const pineOpts: SDK.PineOptionsFor<SDK.DeviceServiceEnvironmentVariable> = {
$expand: {
service_install: {
$expand: 'installs__service',
},
},
};
if (options.service) {
pineOpts.$filter = {
service_install: {
installs__service: { service_name: options.service },
},
};
}
const deviceServiceVars = await sdk.models.device.serviceVar.getAllByDevice(
fullUUID,
pineOpts,
);
fillInInfoFields(deviceServiceVars, appName, printedUUID);
deviceVars.push(...deviceServiceVars);
}
if (!options.service || options.all) {
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceEnvVars, appName, printedUUID);
deviceVars.push(...deviceEnvVars);
}
}
return deviceVars;
}
/**
* For each env var object in varArray, fill in its top-level serviceName
* and deviceUUID fields. An asterisk is used to indicate that the variable
* applies to "all services" or "all devices".
*/
function fillInInfoFields(
varArray:
| EnvironmentVariableInfo[]
| DeviceServiceEnvironmentVariableInfo[]
| ServiceEnvironmentVariableInfo[],
appName?: string,
deviceUUID?: string,
) {
for (const envVar of varArray) {
if ('service' in envVar) {
// envVar is of type ServiceEnvironmentVariableInfo
envVar.serviceName = _.at(envVar as any, 'service[0].service_name')[0];
} else if ('service_install' in envVar) {
// envVar is of type DeviceServiceEnvironmentVariableInfo
envVar.serviceName = _.at(
envVar as any,
'service_install[0].installs__service[0].service_name',
)[0];
}
envVar.appName = appName;
envVar.serviceName = envVar.serviceName || '*';
envVar.deviceUUID = deviceUUID || '*';
}
}
/**
* Transform each object (item) of varArray to preserve only the
* fields (keys) listed in the fields argument.
*/
function stringifyVarArray<T = Dictionary<any>>(
varArray: T[],
fields: string[],
): string {
const transformed = _.map(varArray, (o: Dictionary<any>) =>
_.transform(
o,
(result, value, key) => {
if (fields.includes(key)) {
result[key] = value;
}
},
{} as Dictionary<any>,
),
);
return JSON.stringify(transformed, null, 4);
}

View File

@ -0,0 +1,81 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
import { CommandHelp } from '../../utils/oclif-utils';
// 'Internal' commands are called during the execution of other commands.
// `osinit` is called during `os initialize`
// TODO: These should be refactored to modules/functions, and removed
// See previous `internal sudo` refactor:
// - https://github.com/balena-io/balena-cli/pull/1455/files
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
interface ArgsDef {
image: string;
type: string;
config: string;
}
export default class OsinitCmd extends Command {
public static description = stripIndent`
Do actual init of the device with the preconfigured os image.
Don't use this command directly!
Use \`balena os initialize <image>\` instead.
`;
public static args = [
{
name: 'image',
required: true,
},
{
name: 'type',
required: true,
},
{
name: 'config',
required: true,
},
];
public static usage = (
'internal osinit ' +
new CommandHelp({ args: OsinitCmd.args }).defaultUsage()
).trim();
public static hidden = true;
public static root = true;
public async run() {
const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);
const { initialize } = await import('balena-device-init');
const { getManifest, osProgressHandler } = await import(
'../../utils/helpers'
);
const config = JSON.parse(params.config);
const manifest = await getManifest(params.image, params.type);
const initializeEmitter = await initialize(params.image, manifest, config);
await osProgressHandler(initializeEmitter);
}
}

View File

@ -0,0 +1,46 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
// 'Internal' commands are called during the execution of other commands.
// `scandevices` is called during by `join`,`leave'.
// TODO: These should be refactored to modules/functions, and removed
// See previous `internal sudo` refactor:
// - https://github.com/balena-io/balena-cli/pull/1455/files
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
export default class ScandevicesCmd extends Command {
public static description = stripIndent`
Scan for local balena-enabled devices and show a picker to choose one.
Don't use this command directly!
`;
public static usage = 'internal scandevices';
public static root = true;
public static hidden = true;
public async run() {
const { forms } = await import('balena-sync');
const hostnameOrIp = await forms.selectLocalBalenaOsDevice();
return console.error(`==> Selected device: ${hostnameOrIp}`);
}
}

98
lib/actions-oclif/join.ts Normal file
View File

@ -0,0 +1,98 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk } from '../utils/lazy';
interface FlagsDef {
application?: string;
help?: void;
}
interface ArgsDef {
deviceIpOrHostname?: string;
}
export default class JoinCmd extends Command {
public static description = stripIndent`
Move a local device to an application on another balena server.
Move a local device to an application on another balena server, causing
the device to "join" the new server. The device must be running balenaOS.
For example, you could provision a device against an openBalena installation
where you perform end-to-end tests and then move it to balenaCloud when it's
ready for production.
To move a device between applications on the same server, use the
\`balena device move\` command instead of \`balena join\`.
If you don't specify a device hostname or IP, this command will automatically
scan the local network for balenaOS devices and prompt you to select one
from an interactive picker. This requires root privileges. Likewise, if
the application flag is not provided then a picker will be shown.
`;
public static examples = [
'$ balena join',
'$ balena join balena.local',
'$ balena join balena.local --application MyApp',
'$ balena join 192.168.1.25',
'$ balena join 192.168.1.25 --application MyApp',
];
public static args = [
{
name: 'deviceIpOrHostname',
description: 'the IP or hostname of device',
},
];
// Hardcoded to preserve camelcase
public static usage = 'join [deviceIpOrHostname]';
public static flags: flags.Input<FlagsDef> = {
application: {
description: 'the name of the application the device should join',
...cf.application,
},
help: cf.help,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
JoinCmd,
);
const Logger = await import('../utils/logger');
const promote = await import('../utils/promote');
const sdk = getBalenaSdk();
const logger = Logger.getLogger();
return promote.join(
logger,
sdk,
params.deviceIpOrHostname,
options.application,
);
}
}

View File

@ -0,0 +1,87 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
name: string;
path: string;
}
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.
If \`path\` is omitted, the command will attempt
to read the SSH key from stdin.
`;
public static examples = [
'$ balena key add Main ~/.ssh/id_rsa.pub',
'$ cat ~/.ssh/id_rsa.pub | balena key add Main',
];
public static args = [
{
name: 'name',
description: 'the SSH key name',
required: true,
},
{
name: `path`,
description: `the path to the public key file`,
},
];
public static usage = 'key add <name> [path]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public static readStdin = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(KeyAddCmd);
let key: string;
if (params.path != null) {
const { promisify } = await import('util');
const readFileAsync = promisify((await import('fs')).readFile);
key = await readFileAsync(params.path, 'utf8');
} else if (this.stdin.length > 0) {
key = this.stdin;
} else {
throw new ExpectedError('No public key file or path provided.');
}
await getBalenaSdk().models.key.create(params.name, key);
}
}

View File

@ -0,0 +1,79 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
help: void;
}
interface ArgsDef {
id: number;
}
export default class KeyCmd extends Command {
public static description = stripIndent`
Display an SSH key.
Display a single SSH key registered in balenaCloud for the logged in user.
`;
public static examples = ['$ balena key 17'];
public static args: Array<IArg<any>> = [
{
name: 'id',
description: 'balenaCloud ID for the SSH key',
parse: (x) => parseAsInteger(x, 'id'),
required: true,
},
];
public static usage = 'key <id>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
const key = await getBalenaSdk().models.key.get(params.id);
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
};
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
// Since the public key string is long, it might
// wrap to lines below, causing the table layout to break.
// See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key);
}
}

View File

@ -0,0 +1,79 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
id: number;
}
export default class KeyRmCmd extends Command {
public static description = stripIndent`
Remove an SSH key from balenaCloud.
Remove a single SSH key registered in balenaCloud for the logged in user.
The --yes option may be used to avoid interactive confirmation.
`;
public static examples = ['$ balena key rm 17', '$ balena key rm 17 --yes'];
public static args: Array<IArg<any>> = [
{
name: 'id',
description: 'balenaCloud ID for the SSH key',
parse: (x) => parseAsInteger(x, 'id'),
required: true,
},
];
public static usage = 'key rm <id>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
KeyRmCmd,
);
const patterns = await import('../../utils/patterns');
await patterns.confirm(
options.yes ?? false,
`Are you sure you want to delete key ${params.id}?`,
);
await getBalenaSdk().models.key.remove(params.id);
}
}

56
lib/actions-oclif/keys.ts Normal file
View File

@ -0,0 +1,56 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
interface FlagsDef {
help: void;
}
export default class KeysCmd extends Command {
public static description = stripIndent`
List the SSH keys in balenaCloud.
List all SSH keys registered in balenaCloud for the logged in user.
`;
public static examples = ['$ balena keys'];
public static usage = 'keys';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
this.parse<FlagsDef, {}>(KeysCmd);
const keys = await getBalenaSdk().models.key.getAll();
// Use 'name' instead of 'title' to match dashboard.
const displayKeys: Array<{ id: number; name: string }> = keys.map((k) => {
return { id: k.id, name: k.title };
});
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
}
}

View 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 { stripIndent } from 'common-tags';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk } from '../utils/lazy';
interface FlagsDef {
help?: void;
}
interface ArgsDef {
deviceIpOrHostname?: string;
}
export default class LeaveCmd extends Command {
public static description = stripIndent`
Remove a local device from its balena application.
Remove a local device from its balena application, causing the device to
"leave" the server it is provisioned on. This effectively makes the device
"unmanaged". The device must be running balenaOS.
The device entry on the server is preserved after running this command,
so the device can subsequently re-join the server if needed.
If you don't specify a device hostname or IP, this command will automatically
scan the local network for balenaOS devices and prompt you to select one
from an interactive picker. This usually requires root privileges.
`;
public static examples = [
'$ balena leave',
'$ balena leave balena.local',
'$ balena leave 192.168.1.25',
];
public static args = [
{
name: 'deviceIpOrHostname',
description: 'the device IP or hostname',
},
];
// Hardcoded to preserve camelcase
public static usage = 'leave [deviceIpOrHostname]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(LeaveCmd);
const Logger = await import('../utils/logger');
const promote = await import('../utils/promote');
const sdk = getBalenaSdk();
const logger = Logger.getLogger();
return promote.leave(logger, sdk, params.deviceIpOrHostname);
}
}

94
lib/actions-oclif/note.ts Normal file
View File

@ -0,0 +1,94 @@
/**
* @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 { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk } from '../utils/lazy';
interface FlagsDef {
device?: string; // device UUID
dev?: string; // Alias for device.
help: void;
}
interface ArgsDef {
note: string;
}
export default class NoteCmd extends Command {
public static description = stripIndent`
Set a device note.
Set or update a device note. If the note argument is not provided,
it will be read from stdin.
To view device notes, use the \`balena device <uuid>\` command.
`;
public static examples = [
'$ balena note "My useful note" --device 7cf02a6',
'$ cat note.txt | balena note --device 7cf02a6',
];
public static args = [
{
name: 'note',
description: 'note content',
},
];
public static usage = 'note <|note>';
public static flags: flags.Input<FlagsDef> = {
device: { exclusive: ['dev'], ...cf.device },
dev: flags.string({
exclusive: ['device'],
hidden: true,
}),
help: cf.help,
};
public static authenticated = true;
public static readStdin = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
NoteCmd,
);
params.note = params.note || this.stdin;
if (_.isEmpty(params.note)) {
throw new ExpectedError('Missing note content');
}
options.device = options.device || options.dev;
delete options.dev;
if (_.isEmpty(options.device)) {
throw new ExpectedError('Missing device UUID (--device)');
}
const balena = getBalenaSdk();
return balena.models.device.note(options.device!, params.note);
}
}

View File

@ -15,15 +15,22 @@
* limitations under the License.
*/
import { Command, flags } from '@oclif/command';
import { flags } from '@oclif/command';
import BalenaSdk = require('balena-sdk');
import Bluebird = require('bluebird');
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import * as path from 'path';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
const BOOT_PARTITION = 1;
const CONNECTIONS_FOLDER = '/system-connections';
interface FlagsDef {
advanced?: boolean;
app?: string;
@ -38,6 +45,8 @@ interface FlagsDef {
'device-type'?: string;
help?: void;
version?: string;
'system-connection': string[];
'initial-device-name'?: string;
}
interface ArgsDef {
@ -67,7 +76,7 @@ export default class OsConfigureCmd extends Command {
Configure a previously downloaded balenaOS image for a specific device type or
balena application.
Configuration settings such as WiFi authentication will be taken from the
following sources, in precedence order:
1. Command-line options like \`--config-wifi-ssid\`
@ -77,6 +86,13 @@ export default class OsConfigureCmd extends Command {
The --device-type option may be used to override the application's default
device type, in case of an application with mixed device types.
The --system-connection (-c) option can be used to inject NetworkManager connection
profiles for additional network interfaces, such as cellular/GSM or additional
WiFi or ethernet connections. This option may be passed multiple times in case there
are multiple files to inject. See connection profile examples and reference at:
https://www.balena.io/docs/reference/OS/network/2.x/
https://developer.gnome.org/NetworkManager/stable/nm-settings.html
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
`;
public static examples = [
@ -111,7 +127,7 @@ export default class OsConfigureCmd extends Command {
description: "same as '--application'",
exclusive: ['application', 'device'],
}),
application: _.assign({ exclusive: ['app', 'device'] }, cf.application),
application: { exclusive: ['app', 'device'], ...cf.application },
config: flags.string({
description:
'path to a pre-generated config.json file to be injected in the OS image',
@ -130,7 +146,7 @@ export default class OsConfigureCmd extends Command {
'config-wifi-ssid': flags.string({
description: 'WiFi SSID (network name) (non-interactive configuration)',
}),
device: _.assign({ exclusive: ['app', 'application'] }, cf.device),
device: { exclusive: ['app', 'application'], ...cf.device },
'device-api-key': flags.string({
char: 'k',
description:
@ -140,10 +156,21 @@ export default class OsConfigureCmd extends Command {
description:
'device type slug (e.g. "raspberrypi3") to override the application device type',
}),
'initial-device-name': flags.string({
description:
'This option will set the device name when the device provisions',
}),
help: cf.help,
version: flags.string({
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
}),
'system-connection': flags.string({
multiple: true,
char: 'c',
required: false,
description:
"paths to local files to place into the 'system-connections' directory",
}),
};
public async run() {
@ -157,16 +184,17 @@ export default class OsConfigureCmd extends Command {
await validateOptions(options);
const devInit = await import('balena-device-init');
const balena = (await import('balena-sdk')).fromSharedOptions();
const fs = await import('mz/fs');
const { generateDeviceConfig, generateApplicationConfig } = await import(
'../../utils/config'
);
const helpers = await import('../../utils/helpers');
const imagefs = await require('resin-image-fs');
let app: BalenaSdk.Application | undefined;
let device: BalenaSdk.Device | undefined;
let deviceTypeSlug: string;
const balena = getBalenaSdk();
if (options.device) {
device = await balena.models['device'].get(options.device);
deviceTypeSlug = device.device_type;
@ -211,16 +239,51 @@ export default class OsConfigureCmd extends Command {
}
}
if (
options['initial-device-name'] &&
options['initial-device-name'] !== ''
) {
configJson!.initialDeviceName = options['initial-device-name'];
}
console.info('Configuring operating system image');
const image = params.image;
await helpers.osProgressHandler(
await devInit.configure(
params.image,
image,
deviceTypeManifest,
configJson || {},
answers,
),
);
if (options['system-connection']) {
const files = await Bluebird.map(
options['system-connection'],
async (filePath) => {
const content = await fs.readFile(filePath, 'utf8');
const name = path.basename(filePath);
return {
name,
content,
};
},
);
await Bluebird.each(files, async ({ name, content }) => {
await imagefs.writeFile(
{
image,
partition: BOOT_PARTITION,
path: path.join(CONNECTIONS_FOLDER, name),
},
content,
);
console.info(`Copied system-connection file: ${name}`);
});
}
}
}
@ -257,8 +320,8 @@ async function validateOptions(options: FlagsDef) {
-------------------------------------------------------------------------------------------
`);
}
const { checkLoggedIn } = await import('../../utils/patterns');
await checkLoggedIn();
await Command.checkLoggedIn();
}
/**
@ -306,9 +369,7 @@ async function checkDeviceTypeCompatibility(
const helpers = await import('../../utils/helpers');
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
throw new ExpectedError(
`Device type ${
options['device-type']
} is incompatible with application ${options.application}`,
`Device type ${options['device-type']} is incompatible with application ${options.application}`,
);
}
}
@ -424,7 +485,7 @@ function camelifyConfigOptions(options: FlagsDef): { [key: string]: any } {
if (key.startsWith('config-')) {
return key
.substring('config-'.length)
.replace(/-[a-z]/g, match => match.substring(1).toUpperCase());
.replace(/-[a-z]/g, (match) => match.substring(1).toUpperCase());
}
return key;
});

174
lib/actions-oclif/scan.ts Normal file
View File

@ -0,0 +1,174 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import { LocalBalenaOsDevice } from 'balena-sync';
import { stripIndent } from 'common-tags';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getVisuals } from '../utils/lazy';
interface FlagsDef {
verbose: boolean;
timeout?: number;
help: void;
}
export default class ScanCmd extends Command {
public static description = stripIndent`
Scan for balenaOS devices on your local network.
Scan for balenaOS devices on your local network.
`;
public static examples = [
'$ balena scan',
'$ balena scan --timeout 120',
'$ balena scan --verbose',
];
public static usage = 'scan';
public static flags: flags.Input<FlagsDef> = {
verbose: flags.boolean({
char: 'v',
default: false,
description: 'display full info',
}),
timeout: flags.integer({
char: 't',
description: 'scan timeout in seconds',
}),
help: cf.help,
};
public static primary = true;
public static root = true;
public async run() {
const Bluebird = await import('bluebird');
const _ = await import('lodash');
const { SpinnerPromise } = getVisuals();
const { discover } = await import('balena-sync');
const prettyjson = await import('prettyjson');
const { ExpectedError } = await import('../errors');
const { dockerPort, dockerTimeout } = await import(
'../actions/local/common'
);
const dockerUtils = await import('../utils/docker');
const { flags: options } = this.parse<FlagsDef, {}>(ScanCmd);
const discoverTimeout =
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;
}
});
// 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,
);
}
// Query devices for info
const devicesInfo = await Bluebird.map(
activeLocalDevices,
({ host, address }) => {
const docker = dockerUtils.createClient({
host: address,
port: dockerPort,
timeout: dockerTimeout,
}) as any;
return Bluebird.props({
host,
address,
dockerInfo: docker
.infoAsync()
.catchReturn('Could not get Docker info'),
dockerVersion: docker
.versionAsync()
.catchReturn('Could not get Docker version'),
});
},
);
// Reduce properties if not --verbose
if (!options.verbose) {
devicesInfo.forEach((d: any) => {
d.dockerInfo = _.isObject(d.dockerInfo)
? _.pick(d.dockerInfo, ScanCmd.dockerInfoProperties)
: d.dockerInfo;
d.dockerVersion = _.isObject(d.dockerVersion)
? _.pick(d.dockerVersion, ScanCmd.dockerVersionProperties)
: d.dockerVersion;
});
}
// Output results
console.log(prettyjson.render(devicesInfo, { noColor: true }));
}
protected static dockerInfoProperties = [
'Containers',
'ContainersRunning',
'ContainersPaused',
'ContainersStopped',
'Images',
'Driver',
'SystemTime',
'KernelVersion',
'OperatingSystem',
'Architecture',
];
protected static dockerVersionProperties = ['Version', 'ApiVersion'];
protected static noDevicesFoundMessage =
'Could not find any balenaOS devices on the local network.';
protected static windowsTipMessage = `
Note for Windows users:
The 'scan' command relies on the Bonjour service. Check whether Bonjour is
installed (Control Panel > Programs and Features). If not, you can download
Bonjour for Windows (included with Bonjour Print Services) from here:
https://support.apple.com/kb/DL999
After installing Bonjour, restart your PC and run the 'balena scan' command
again.`;
}

View File

@ -0,0 +1,52 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk } from '../utils/lazy';
interface FlagsDef {
help: void;
}
export default class SettingsCmd extends Command {
public static description = stripIndent`
Print current settings.
Use this command to display current balena CLI settings.
`;
public static examples = ['$ balena settings'];
public static usage = 'settings';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public async run() {
this.parse<FlagsDef, {}>(SettingsCmd);
const prettyjson = await import('prettyjson');
return getBalenaSdk()
.settings.getAll()
.then(prettyjson.render)
.then(console.log);
}
}

134
lib/actions-oclif/tag/rm.ts Normal file
View File

@ -0,0 +1,134 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { disambiguateReleaseParam } from '../../utils/normalization';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
application?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
interface ArgsDef {
tagKey: string;
}
export default class TagRmCmd extends Command {
public static description = stripIndent`
Remove a tag from an application, device or release.
Remove a tag from an application, device or release.
`;
public static examples = [
'$ balena tag rm myTagKey --application MyApp',
'$ balena tag rm myTagKey --device 7cf02a6',
'$ balena tag rm myTagKey --release 1234',
'$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6',
];
public static args = [
{
name: 'tagKey',
description: 'the key string of the tag',
required: true,
},
];
public static usage = 'tag rm <tagKey>';
public static flags: flags.Input<FlagsDef> = {
application: {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
},
help: cf.help,
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device', 'release'],
}),
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
TagRmCmd,
);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
throw new ExpectedError(TagRmCmd.missingResourceMessage);
}
if (options.application) {
return balena.models.application.tags.remove(
tryAsInteger(options.application),
params.tagKey,
);
}
if (options.device) {
return balena.models.device.tags.remove(
tryAsInteger(options.device),
params.tagKey,
);
}
if (options.release) {
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
);
return balena.models.release.tags.remove(releaseParam, params.tagKey);
}
}
protected static missingResourceMessage = stripIndent`
To remove a resource tag, you must provide exactly one of:
* An application, with --application <appname>
* A device, with --device <uuid>
* A release, with --release <id or commit>
See the help page for examples:
$ balena help tag rm
`;
}

View File

@ -0,0 +1,157 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { disambiguateReleaseParam } from '../../utils/normalization';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
application?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
interface ArgsDef {
tagKey: string;
value?: string;
}
export default class TagSetCmd extends Command {
public static description = stripIndent`
Set a tag on an application, device or release.
Set a tag on an application, device or release.
You can optionally provide a value to be associated with the created
tag, as an extra argument after the tag key. If a value isn't
provided, a tag with an empty value is created.
`;
public static examples = [
'$ balena tag set mySimpleTag --application MyApp',
'$ balena tag set myCompositeTag myTagValue --application MyApp',
'$ balena tag set myCompositeTag myTagValue --device 7cf02a6',
'$ balena tag set myCompositeTag "my tag value with whitespaces" --device 7cf02a6',
'$ balena tag set myCompositeTag myTagValue --release 1234',
'$ balena tag set myCompositeTag --release 1234',
'$ balena tag set myCompositeTag --release b376b0e544e9429483b656490e5b9443b4349bd6',
];
public static args = [
{
name: 'tagKey',
description: 'the key string of the tag',
required: true,
},
{
name: 'value',
description: 'the optional value associated with the tag',
required: false,
},
];
public static usage = 'tag set <tagKey> [value]';
public static flags: flags.Input<FlagsDef> = {
application: {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
},
help: cf.help,
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device', 'release'],
}),
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
TagSetCmd,
);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
throw new ExpectedError(TagSetCmd.missingResourceMessage);
}
if (params.value == null) {
params.value = '';
}
if (options.application) {
return balena.models.application.tags.set(
tryAsInteger(options.application),
params.tagKey,
params.value,
);
}
if (options.device) {
return balena.models.device.tags.set(
tryAsInteger(options.device),
params.tagKey,
params.value,
);
}
if (options.release) {
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
);
return balena.models.release.tags.set(
releaseParam,
params.tagKey,
params.value,
);
}
}
protected static missingResourceMessage = stripIndent`
To set a resource tag, you must provide exactly one of:
* An application, with --application <appname>
* A device, with --device <uuid>
* A release, with --release <id or commit>
See the help page for examples:
$ balena help tag set
`;
}

132
lib/actions-oclif/tags.ts Normal file
View File

@ -0,0 +1,132 @@
/**
* @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 { stripIndent } from 'common-tags';
import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
import { disambiguateReleaseParam } from '../utils/normalization';
import { tryAsInteger } from '../utils/validation';
import { isV12 } from '../utils/version';
interface FlagsDef {
application?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
export default class TagsCmd extends Command {
public static description = stripIndent`
List all tags for an application, device or release.
List all tags and their values for a particular application,
device or release.
`;
public static examples = [
'$ balena tags --application MyApp',
'$ balena tags --device 7cf02a6',
'$ balena tags --release 1234',
'$ balena tags --release b376b0e544e9429483b656490e5b9443b4349bd6',
];
public static usage = 'tags';
public static flags: flags.Input<FlagsDef> = {
application: {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
},
help: cf.help,
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device', 'release'],
}),
};
public static authenticated = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
throw new ExpectedError(this.missingResourceMessage);
}
let tags;
if (options.application) {
tags = await balena.models.application.tags.getAllByApplication(
tryAsInteger(options.application),
);
}
if (options.device) {
tags = await balena.models.device.tags.getAllByDevice(
tryAsInteger(options.device),
);
}
if (options.release) {
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
);
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
}
if (!tags || tags.length === 0) {
throw new ExpectedError('No tags found');
}
console.log(
isV12()
? getVisuals().table.horizontal(tags, ['tag_key', 'value'])
: getVisuals().table.horizontal(tags, ['id', 'tag_key', 'value']),
);
}
protected missingResourceMessage = stripIndent`
To list tags for a resource, you must provide exactly one of:
* An application, with --application <appname>
* A device, with --device <uuid>
* A release, with --release <id or commit>
See the help page for examples:
$ balena help tags
`;
}

View File

@ -15,8 +15,9 @@
* limitations under the License.
*/
import { Command, flags } from '@oclif/command';
import { flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import Command from '../command';
interface FlagsDef {
all?: boolean;
@ -34,8 +35,11 @@ export default class VersionCmd extends Command {
Display version information for the balena CLI and/or Node.js.
Display version information for the balena CLI and/or Node.js.
If you intend to parse the output, please use the -j option for
JSON output, as its format is more stable.
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. The 'jq' utility may be helpful
in shell scripts (https://stedolan.github.io/jq/manual/).
`;
public static examples = [
'$ balena version',

View File

@ -1,36 +0,0 @@
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
export const generate: CommandDefinition<{
name: string;
}> = {
signature: 'api-key generate <name>',
description: 'Generate a new API key with the given name',
help: stripIndent`
This command generates a new API key for the current user, with the given
name. The key will be logged to the console.
This key can be used to log into the CLI using 'balena login --token <key>',
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
Examples:
$ balena api-key generate "Jenkins Key"
`,
async action(params, _options, done) {
const balena = (await import('balena-sdk')).fromSharedOptions();
balena.models.apiKey
.create(params.name)
.then(key => {
console.log(stripIndent`
Registered api key '${params.name}':
${key}
This key will not be shown again, so please save it now.
`);
})
.finally(done);
},
};

View File

@ -1,173 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
commandOptions = require('./command-options')
exports.create =
signature: 'app create <name>'
description: 'create an application'
help: '''
Use this command to create a new balena application.
You can specify the application device type with the `--type` option.
Otherwise, an interactive dropdown will be shown for you to select from.
You can see a list of supported device types with
$ balena devices supported
Examples:
$ balena app create MyApp
$ balena app create MyApp --type raspberry-pi
'''
options: [
{
signature: 'type'
parameter: 'type'
description: 'application device type (Check available types with `balena devices supported`)'
alias: 't'
}
]
permission: 'user'
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
# Validate the the application name is available
# before asking the device type.
# https://github.com/balena-io/balena-cli/issues/30
balena.models.application.has(params.name).then (hasApplication) ->
if hasApplication
patterns.exitWithExpectedError('You already have an application with that name!')
.then ->
return options.type or patterns.selectDeviceType()
.then (deviceType) ->
return balena.models.application.create({
name: params.name
deviceType
})
.then (application) ->
console.info("Application created: #{application.app_name} (#{application.device_type}, id #{application.id})")
.nodeify(done)
exports.list =
signature: 'apps'
description: 'list all applications'
help: '''
Use this command to list all your applications.
Notice this command only shows the most important bits of information for each app.
If you want detailed information, use balena app <name> instead.
Examples:
$ balena apps
'''
permission: 'user'
primary: true
action: (params, options, done) ->
_ = require('lodash')
balena = require('balena-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
balena.models.application.getAll
$select: [
'id'
'app_name'
'device_type'
]
$expand: owns__device: $select: 'is_online'
.then (applications) ->
applications.forEach (application) ->
application.device_count = _.size(application.owns__device)
application.online_devices = _.filter(application.owns__device, (d) -> d.is_online == true).length
console.log visuals.table.horizontal applications, [
'id'
'app_name'
'device_type'
'online_devices'
'device_count'
]
.nodeify(done)
exports.info =
signature: 'app <name>'
description: 'list a single application'
help: '''
Use this command to show detailed information for a single application.
Examples:
$ balena app MyApp
'''
permission: 'user'
primary: true
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
balena.models.application.get(params.name).then (application) ->
console.log visuals.table.vertical application, [
"$#{application.app_name}$"
'id'
'device_type'
'git_repository'
'commit'
]
.nodeify(done)
exports.restart =
signature: 'app restart <name>'
description: 'restart an application'
help: '''
Use this command to restart all devices that belongs to a certain application.
Examples:
$ balena app restart MyApp
'''
permission: 'user'
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
balena.models.application.restart(params.name).nodeify(done)
exports.remove =
signature: 'app rm <name>'
description: 'remove an application'
help: '''
Use this command to remove a balena application.
Notice this command asks for confirmation interactively.
You can avoid this by passing the `--yes` boolean option.
Examples:
$ balena app rm MyApp
$ balena app rm MyApp --yes
'''
options: [ commandOptions.yes ]
permission: 'user'
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
patterns.confirm(options.yes, 'Are you sure you want to delete the application?').then ->
balena.models.application.remove(params.name)
.nodeify(done)

View File

@ -1,170 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
exports.login =
signature: 'login'
description: 'login to balena'
help: '''
Use this command to login to your balena account.
This command will prompt you to login using the following login types:
- Web authorization: open your web browser and prompt you to authorize the CLI
from the dashboard.
- Credentials: using email/password and 2FA.
- Token: using a session token or API key from the preferences page.
Examples:
$ balena login
$ balena login --web
$ balena login --token "..."
$ balena login --credentials
$ balena login --credentials --email johndoe@gmail.com --password secret
'''
options: [
{
signature: 'token'
description: 'session token or API key'
parameter: 'token'
alias: 't'
}
{
signature: 'web'
description: 'web-based login'
boolean: true
alias: 'w'
}
{
signature: 'credentials'
description: 'credential-based login'
boolean: true
alias: 'c'
}
{
signature: 'email'
parameter: 'email'
description: 'email'
alias: [ 'e', 'u' ]
}
{
signature: 'password'
parameter: 'password'
description: 'password'
alias: 'p'
}
]
primary: true
action: (params, options, done) ->
_ = require('lodash')
Promise = require('bluebird')
balena = require('balena-sdk').fromSharedOptions()
auth = require('../auth')
form = require('resin-cli-form')
patterns = require('../utils/patterns')
messages = require('../utils/messages')
login = (options) ->
if options.token?
return Promise.try ->
return options.token if _.isString(options.token)
return form.ask
message: 'Session token or API key from the preferences page'
name: 'token'
type: 'input'
.then(balena.auth.loginWithToken)
.tap ->
balena.auth.whoami()
.then (username) ->
if !username
patterns.exitWithExpectedError('Token authentication failed')
else if options.credentials
return patterns.authenticate(options)
else if options.web
console.info('Connecting to the web dashboard')
return auth.login()
return patterns.askLoginType().then (loginType) ->
if loginType is 'register'
signupUrl = 'https://dashboard.balena-cloud.com/signup'
require('opn')(signupUrl, { wait: false })
patterns.exitWithExpectedError("Please sign up at #{signupUrl}")
options[loginType] = true
return login(options)
balena.settings.get('balenaUrl').then (balenaUrl) ->
console.log(messages.balenaAsciiArt)
console.log("\nLogging in to #{balenaUrl}")
return login(options)
.then(balena.auth.whoami)
.tap (username) ->
console.info("Successfully logged in as: #{username}")
console.info """
Find out about the available commands by running:
$ balena help
#{messages.reachingOut}
"""
.nodeify(done)
exports.logout =
signature: 'logout'
description: 'logout from balena'
help: '''
Use this command to logout from your balena account.
Examples:
$ balena logout
'''
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
balena.auth.logout().nodeify(done)
exports.whoami =
signature: 'whoami'
description: 'get current username and email address'
help: '''
Use this command to find out the current logged in username and email address.
Examples:
$ balena whoami
'''
permission: 'user'
action: (params, options, done) ->
Promise = require('bluebird')
balena = require('balena-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
Promise.props
username: balena.auth.whoami()
email: balena.auth.getEmail()
url: balena.settings.get('balenaUrl')
.then (results) ->
console.log visuals.table.vertical results, [
'$account information$'
'username'
'email'
'url'
]
.nodeify(done)

196
lib/actions/auth.ts Normal file
View File

@ -0,0 +1,196 @@
/*
Copyright 2016-2020 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { CommandDefinition } from 'capitano';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
export const login: CommandDefinition<
{},
{
token: string | boolean;
web: boolean;
credentials: boolean;
email: string;
password: string;
}
> = {
signature: 'login',
description: 'login to balena',
help: `\
Use this command to login to your balena account.
This command will prompt you to login using the following login types:
- Web authorization: open your web browser and prompt you to authorize the CLI
from the dashboard.
- Credentials: using email/password and 2FA.
- Token: using a session token or API key from the preferences page.
Examples:
$ balena login
$ balena login --web
$ balena login --token "..."
$ balena login --credentials
$ balena login --credentials --email johndoe@gmail.com --password secret\
`,
options: [
{
signature: 'token',
description: 'session token or API key',
parameter: 'token',
alias: 't',
},
{
signature: 'web',
description: 'web-based login',
boolean: true,
alias: 'w',
},
{
signature: 'credentials',
description: 'credential-based login',
boolean: true,
alias: 'c',
},
{
signature: 'email',
parameter: 'email',
description: 'email',
alias: ['e', 'u'],
},
{
signature: 'password',
parameter: 'password',
description: 'password',
alias: 'p',
},
],
primary: true,
async action(_params, options) {
type Options = typeof options;
const balena = getBalenaSdk();
const patterns = await import('../utils/patterns');
const messages = await import('../utils/messages');
const { exitWithExpectedError } = await import('../errors');
const doLogin = async (loginOptions: Options): Promise<void> => {
if (loginOptions.token != null) {
let token: string;
if (typeof loginOptions.token === 'string') {
token = loginOptions.token;
} else {
const form = await import('resin-cli-form');
token = await form.ask({
message: 'Session token or API key from the preferences page',
name: 'token',
type: 'input',
});
}
await balena.auth.loginWithToken(token);
if (!(await balena.auth.whoami())) {
exitWithExpectedError('Token authentication failed');
}
return;
} else if (loginOptions.credentials) {
return patterns.authenticate(loginOptions);
} else if (loginOptions.web) {
const auth = await import('../auth');
await auth.login();
return;
}
const loginType = await patterns.askLoginType();
if (loginType === 'register') {
const signupUrl = 'https://dashboard.balena-cloud.com/signup';
const open = await import('open');
open(signupUrl, { wait: false });
return exitWithExpectedError(`Please sign up at ${signupUrl}`);
}
loginOptions[loginType] = true;
return doLogin(loginOptions);
};
const balenaUrl = await balena.settings.get('balenaUrl');
console.log(messages.balenaAsciiArt);
console.log(`\nLogging in to ${balenaUrl}`);
await doLogin(options);
const username = await balena.auth.whoami();
console.info(`Successfully logged in as: ${username}`);
console.info(`\
Find out about the available commands by running:
$ balena help
${messages.reachingOut}`);
if (options.web) {
const { shutdownServer } = await import('../auth');
shutdownServer();
}
},
};
export const logout: CommandDefinition = {
signature: 'logout',
description: 'logout from balena',
help: `\
Use this command to logout from your balena account.
Examples:
$ balena logout\
`,
async action(_params) {
await getBalenaSdk().auth.logout();
},
};
export const whoami: CommandDefinition = {
signature: 'whoami',
description: 'get current username and email address',
help: `\
Use this command to find out the current logged in username and email address.
Examples:
$ balena whoami\
`,
permission: 'user',
async action() {
const balena = getBalenaSdk();
const [username, email, url] = await Promise.all([
balena.auth.whoami(),
balena.auth.getEmail(),
balena.settings.get('balenaUrl'),
]);
console.log(
getVisuals().table.vertical({ username, email, url }, [
'$account information$',
'username',
'email',
'url',
]),
);
},
};

View File

@ -1,155 +0,0 @@
# Imported here because it's needed for the setup
# of this action
Promise = require('bluebird')
dockerUtils = require('../utils/docker')
compose = require('../utils/compose')
{ registrySecretsHelp } = require('../utils/messages')
###
Opts must be an object with the following keys:
app: the app this build is for (optional)
arch: the architecture to build for
deviceType: the device type to build for
buildEmulated
buildOpts: arguments to forward to docker build command
###
buildProject = (docker, logger, composeOpts, opts) ->
compose.loadProject(
logger
composeOpts.projectPath
composeOpts.projectName
undefined # image: name of pre-built image
composeOpts.dockerfilePath # ok if undefined
)
.then (project) ->
appType = opts.app?.application_type?[0]
if appType? and project.descriptors.length > 1 and not appType.supports_multicontainer
logger.logWarn(
'Target application does not support multiple containers.\n' +
'Continuing with build, but you will not be able to deploy.'
)
compose.buildProject(
docker
logger
project.path
project.name
project.composition
opts.arch
opts.deviceType
opts.buildEmulated
opts.buildOpts
composeOpts.inlineLogs
)
.then ->
logger.logSuccess('Build succeeded!')
.tapCatch (e) ->
logger.logError('Build failed')
module.exports =
signature: 'build [source]'
description: 'Build a single image or a multicontainer project locally'
primary: true
help: """
Use this command to build an image or a complete multicontainer project with
the provided docker daemon in your development machine or balena device.
(See also the `balena push` command for the option of building images in the
balenaCloud build servers.)
You must provide either an application or a device-type/architecture pair to use
the balena Dockerfile pre-processor (e.g. Dockerfile.template -> Dockerfile).
This command will look into the given source directory (or the current working
directory if one isn't specified) for a docker-compose.yml file. If it is found,
this command will build each service defined in the compose file. If a compose
file isn't found, the command will look for a Dockerfile[.template] file (or
alternative Dockerfile specified with the `-f` option), and if yet that isn't
found, it will try to generate one.
#{registrySecretsHelp}
Examples:
$ balena build
$ balena build ./source/
$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated
$ balena build --application MyApp ./source/
$ balena build --docker /var/run/docker.sock # Linux, Mac
$ balena build --docker //./pipe/docker_engine # Windows
$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
"""
options: dockerUtils.appendOptions compose.appendOptions [
{
signature: 'arch'
parameter: 'arch'
description: 'The architecture to build for'
alias: 'A'
},
{
signature: 'deviceType'
parameter: 'deviceType'
description: 'The type of device this build is for'
alias: 'd'
},
{
signature: 'application'
parameter: 'application'
description: 'The target balena application this build is for'
alias: 'a'
},
]
action: (params, options, done) ->
# compositions with many services trigger misleading warnings
require('events').defaultMaxListeners = 1000
sdk = (require('balena-sdk')).fromSharedOptions()
{ validateComposeOptions } = require('../utils/compose_ts')
{ exitWithExpectedError } = require('../utils/patterns')
helpers = require('../utils/helpers')
Logger = require('../utils/logger')
logger = Logger.getLogger()
logger.logDebug('Parsing input...')
# `build` accepts `[source]` as a parameter, but compose expects it
# as an option. swap them here
options.source ?= params.source
delete params.source
Promise.resolve(validateComposeOptions(sdk, options))
.then ->
{ application, arch, deviceType } = options
if (not (arch? and deviceType?) and not application?) or (application? and (arch? or deviceType?))
exitWithExpectedError('You must specify either an application or an arch/deviceType pair to build for')
if arch? and deviceType?
[ undefined, arch, deviceType ]
else
Promise.join(
helpers.getApplication(application)
helpers.getArchAndDeviceType(application)
(app, { arch, device_type }) ->
app.arch = arch
app.device_type = device_type
return app
)
.then (app) ->
[ app, app.arch, app.device_type ]
.then ([ app, arch, deviceType ]) ->
Promise.join(
dockerUtils.getDocker(options)
dockerUtils.generateBuildOpts(options)
compose.generateOpts(options)
(docker, buildOpts, composeOpts) ->
buildProject(docker, logger, composeOpts, {
app
arch
deviceType
buildEmulated: !!options.emulated
buildOpts
})
)
.asCallback(done)

207
lib/actions/build.js Normal file
View File

@ -0,0 +1,207 @@
/**
* @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.
*/
// Imported here because it's needed for the setup
// of this action
import * as Promise from 'bluebird';
import * as dockerUtils from '../utils/docker';
import * as compose from '../utils/compose';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import { getBalenaSdk } from '../utils/lazy';
/*
Opts must be an object with the following keys:
app: the app this build is for (optional)
arch: the architecture to build for
deviceType: the device type to build for
buildEmulated
buildOpts: arguments to forward to docker build command
*/
const buildProject = function (docker, logger, composeOpts, opts) {
const { loadProject } = require('../utils/compose_ts');
return Promise.resolve(loadProject(logger, composeOpts))
.then(function (project) {
const appType = opts.app?.application_type?.[0];
if (
appType != null &&
project.descriptors.length > 1 &&
!appType.supports_multicontainer
) {
logger.logWarn(
'Target application does not support multiple containers.\n' +
'Continuing with build, but you will not be able to deploy.',
);
}
return compose.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,
);
})
.then(function () {
logger.outputDeferredMessages();
logger.logSuccess('Build succeeded!');
})
.tapCatch(() => {
logger.logError('Build failed');
});
};
export const build = {
signature: 'build [source]',
description: 'Build a single image or a multicontainer project locally',
primary: true,
help: `\
Use this command to build an image or a complete multicontainer project with
the provided docker daemon in your development machine or balena device.
(See also the \`balena push\` command for the option of building images in the
balenaCloud build servers.)
You must provide either an application or a device-type/architecture pair to use
the balena Dockerfile pre-processor (e.g. Dockerfile.template -> Dockerfile).
This command will look into the given source directory (or the current working
directory if one isn't specified) for a docker-compose.yml file, and if found,
each service defined in the compose file will be built. If a compose file isn't
found, it will look for a Dockerfile[.template] file (or alternative Dockerfile
specified with the \`--dockerfile\` option), and if no dockerfile is found, it
will try to generate one.
${registrySecretsHelp}
${dockerignoreHelp}
Examples:
$ balena build
$ balena build ./source/
$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated
$ balena build --application MyApp ./source/
$ balena build --docker /var/run/docker.sock # Linux, Mac
$ balena build --docker //./pipe/docker_engine # Windows
$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem\
`,
options: dockerUtils.appendOptions(
compose.appendOptions([
{
signature: 'arch',
parameter: 'arch',
description: 'The architecture to build for',
alias: 'A',
},
{
signature: 'deviceType',
parameter: 'deviceType',
description: 'The type of device this build is for',
alias: 'd',
},
{
signature: 'application',
parameter: 'application',
description: 'The target balena application this build is for',
alias: 'a',
},
]),
),
action(params, options) {
// compositions with many services trigger misleading warnings
// @ts-ignore editing property that isn't typed but does exist
require('events').defaultMaxListeners = 1000;
const sdk = getBalenaSdk();
const { ExpectedError } = require('../errors');
const { checkLoggedIn } = require('../utils/patterns');
const { validateProjectDirectory } = require('../utils/compose_ts');
const helpers = require('../utils/helpers');
const Logger = require('../utils/logger');
const logger = Logger.getLogger();
logger.logDebug('Parsing input...');
// `build` accepts `[source]` as a parameter, but compose expects it
// as an option. swap them here
if (options.source == null) {
options.source = params.source;
}
delete params.source;
const { application, arch, deviceType } = options;
return Promise.try(function () {
if (
(application == null && (arch == null || deviceType == null)) ||
(application != null && (arch != null || deviceType != null))
) {
throw new ExpectedError(
'You must specify either an application or an arch/deviceType pair to build for',
);
}
if (application) {
return checkLoggedIn();
}
})
.then(() =>
validateProjectDirectory(sdk, {
dockerfilePath: options.dockerfile,
noParentCheck: options['noparent-check'] || false,
projectPath: options.source || '.',
registrySecretsPath: options['registry-secrets'],
}),
)
.then(function ({ dockerfilePath, registrySecrets }) {
options.dockerfile = dockerfilePath;
options['registry-secrets'] = registrySecrets;
if (arch != null && deviceType != null) {
return [undefined, arch, deviceType];
} else {
return helpers
.getAppWithArch(application)
.then((app) => [app, app.arch, app.device_type]);
}
})
.then(function ([app, resolvedArch, resolvedDeviceType]) {
return Promise.join(
dockerUtils.getDocker(options),
dockerUtils.generateBuildOpts(options),
compose.generateOpts(options),
(docker, buildOpts, composeOpts) =>
buildProject(docker, logger, composeOpts, {
app,
arch: resolvedArch,
deviceType: resolvedDeviceType,
buildEmulated: !!options.emulated,
buildOpts,
}),
);
});
},
};

View File

@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import _ = require('lodash');
export const yes = {
signature: 'yes',
description: 'confirm non interactively',
@ -34,10 +32,10 @@ export const optionalApplication = {
alias: ['a', 'app'],
};
export const application = _.defaults(
{ required: 'You have to specify an application' },
optionalApplication,
);
export const application = {
...optionalApplication,
required: 'You have to specify an application',
};
export const optionalRelease = {
signature: 'release',
@ -75,12 +73,10 @@ export const optionalOsVersion = {
export type OptionalOsVersionOption = Partial<OsVersionOption>;
export const osVersion = _.defaults(
{
required: 'You have to specify an exact os version',
},
exports.optionalOsVersion,
);
export const osVersion = {
...exports.optionalOsVersion,
required: 'You have to specify an exact os version',
};
export interface OsVersionOption {
version?: string;

View File

@ -1,357 +0,0 @@
###
Copyright 2016-2018 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.
###
commandOptions = require('./command-options')
{ normalizeUuidProp } = require('../utils/normalization')
exports.read =
signature: 'config read'
description: 'read a device configuration'
help: '''
Use this command to read the config.json file from the mounted filesystem (e.g. SD card) of a provisioned device"
Examples:
$ balena config read --type raspberry-pi
$ balena config read --type raspberry-pi --drive /dev/disk2
'''
options: [
{
signature: 'type'
description: 'device type (Check available types with `balena devices supported`)'
parameter: 'type'
alias: 't'
required: 'You have to specify a device type'
}
{
signature: 'drive'
description: 'drive'
parameter: 'drive'
alias: 'd'
}
]
permission: 'user'
root: true
action: (params, options, done) ->
Promise = require('bluebird')
config = require('balena-config-json')
visuals = require('resin-cli-visuals')
umountAsync = Promise.promisify(require('umount').umount)
prettyjson = require('prettyjson')
Promise.try ->
return options.drive or visuals.drive('Select the device drive')
.tap(umountAsync)
.then (drive) ->
return config.read(drive, options.type)
.tap (configJSON) ->
console.info(prettyjson.render(configJSON))
.nodeify(done)
exports.write =
signature: 'config write <key> <value>'
description: 'write a device configuration'
help: '''
Use this command to write the config.json file to the mounted filesystem (e.g. SD card) of a provisioned device
Examples:
$ balena config write --type raspberry-pi username johndoe
$ balena config write --type raspberry-pi --drive /dev/disk2 username johndoe
$ balena config write --type raspberry-pi files.network/settings "..."
'''
options: [
{
signature: 'type'
description: 'device type (Check available types with `balena devices supported`)'
parameter: 'type'
alias: 't'
required: 'You have to specify a device type'
}
{
signature: 'drive'
description: 'drive'
parameter: 'drive'
alias: 'd'
}
]
permission: 'user'
root: true
action: (params, options, done) ->
Promise = require('bluebird')
_ = require('lodash')
config = require('balena-config-json')
visuals = require('resin-cli-visuals')
umountAsync = Promise.promisify(require('umount').umount)
Promise.try ->
return options.drive or visuals.drive('Select the device drive')
.tap(umountAsync)
.then (drive) ->
config.read(drive, options.type).then (configJSON) ->
console.info("Setting #{params.key} to #{params.value}")
_.set(configJSON, params.key, params.value)
return configJSON
.tap ->
return umountAsync(drive)
.then (configJSON) ->
return config.write(drive, options.type, configJSON)
.tap ->
console.info('Done')
.nodeify(done)
exports.inject =
signature: 'config inject <file>'
description: 'inject a device configuration file'
help: '''
Use this command to inject a config.json file to the mounted filesystem
(e.g. SD card or mounted balenaOS image) of a provisioned device"
Examples:
$ balena config inject my/config.json --type raspberry-pi
$ balena config inject my/config.json --type raspberry-pi --drive /dev/disk2
'''
options: [
{
signature: 'type'
description: 'device type (Check available types with `balena devices supported`)'
parameter: 'type'
alias: 't'
required: 'You have to specify a device type'
}
{
signature: 'drive'
description: 'drive'
parameter: 'drive'
alias: 'd'
}
]
permission: 'user'
root: true
action: (params, options, done) ->
Promise = require('bluebird')
config = require('balena-config-json')
visuals = require('resin-cli-visuals')
umountAsync = Promise.promisify(require('umount').umount)
readFileAsync = Promise.promisify(require('fs').readFile)
Promise.try ->
return options.drive or visuals.drive('Select the device drive')
.tap(umountAsync)
.then (drive) ->
readFileAsync(params.file, 'utf8').then(JSON.parse).then (configJSON) ->
return config.write(drive, options.type, configJSON)
.tap ->
console.info('Done')
.nodeify(done)
exports.reconfigure =
signature: 'config reconfigure'
description: 'reconfigure a provisioned device'
help: '''
Use this command to reconfigure a provisioned device
Examples:
$ balena config reconfigure --type raspberry-pi
$ balena config reconfigure --type raspberry-pi --advanced
$ balena config reconfigure --type raspberry-pi --drive /dev/disk2
'''
options: [
{
signature: 'type'
description: 'device type (Check available types with `balena devices supported`)'
parameter: 'type'
alias: 't'
required: 'You have to specify a device type'
}
{
signature: 'drive'
description: 'drive'
parameter: 'drive'
alias: 'd'
}
{
signature: 'advanced'
description: 'show advanced commands'
boolean: true
alias: 'v'
}
]
permission: 'user'
root: true
action: (params, options, done) ->
Promise = require('bluebird')
config = require('balena-config-json')
visuals = require('resin-cli-visuals')
{ runCommand } = require('../utils/helpers')
umountAsync = Promise.promisify(require('umount').umount)
Promise.try ->
return options.drive or visuals.drive('Select the device drive')
.tap(umountAsync)
.then (drive) ->
config.read(drive, options.type).get('uuid')
.tap ->
umountAsync(drive)
.then (uuid) ->
configureCommand = "os configure #{drive} --device #{uuid}"
if options.advanced
configureCommand += ' --advanced'
return runCommand(configureCommand)
.then ->
console.info('Done')
.nodeify(done)
exports.generate =
signature: 'config generate'
description: 'generate a config.json file'
help: '''
Use this command to generate a config.json for a device or application.
Calling this command with the exact version number of the targeted image is required.
This is interactive by default, but you can do this automatically without interactivity
by specifying an option for each question on the command line, if you know the questions
that will be asked for the relevant device type.
In case that you want to configure an image for an application with mixed device types,
you can pass the --device-type argument along with --app to specify the target device type.
Examples:
$ balena config generate --device 7cf02a6 --version 2.12.7
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
$ balena config generate --app MyApp --version 2.12.7
$ balena config generate --app MyApp --version 2.12.7 --device-type fincm3
$ balena config generate --app MyApp --version 2.12.7 --output config.json
$ balena config generate --app MyApp --version 2.12.7 \
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
'''
options: [
commandOptions.osVersion
commandOptions.optionalApplication
commandOptions.optionalDevice
commandOptions.optionalDeviceApiKey
commandOptions.optionalDeviceType
{
signature: 'generate-device-api-key'
description: 'generate a fresh device key for the device'
boolean: true
}
{
signature: 'output'
description: 'output'
parameter: 'output'
alias: 'o'
}
# Options for non-interactive configuration
{
signature: 'network'
description: 'the network type to use: ethernet or wifi'
parameter: 'network'
}
{
signature: 'wifiSsid'
description: 'the wifi ssid to use (used only if --network is set to wifi)'
parameter: 'wifiSsid'
}
{
signature: 'wifiKey'
description: 'the wifi key to use (used only if --network is set to wifi)'
parameter: 'wifiKey'
}
{
signature: 'appUpdatePollInterval'
description: 'how frequently (in minutes) to poll for application updates'
parameter: 'appUpdatePollInterval'
}
]
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(options, 'device')
Promise = require('bluebird')
writeFileAsync = Promise.promisify(require('fs').writeFile)
balena = require('balena-sdk').fromSharedOptions()
form = require('resin-cli-form')
prettyjson = require('prettyjson')
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
helpers = require('../utils/helpers')
{ exitWithExpectedError } = require('../utils/patterns')
if not options.device? and not options.application?
exitWithExpectedError '''
You have to pass either a device or an application.
See the help page for examples:
$ balena help config generate
'''
if !options.application and options.deviceType
exitWithExpectedError '''
Specifying a different device type is only supported when
generating a config for an application:
* An application, with --app <appname>
* A specific device type, with --device-type <deviceTypeSlug>
See the help page for examples:
$ balena help config generate
'''
Promise.try ->
if options.device?
return balena.models.device.get(options.device)
return balena.models.application.get(options.application)
.then (resource) ->
deviceType = options.deviceType || resource.device_type
manifestPromise = balena.models.device.getManifestBySlug(deviceType)
if options.application && options.deviceType
app = resource
appManifestPromise = balena.models.device.getManifestBySlug(app.device_type)
manifestPromise = manifestPromise.tap (paramDeviceType) ->
appManifestPromise.then (appDeviceType) ->
if not helpers.areDeviceTypesCompatible(appDeviceType, paramDeviceType)
throw new balena.errors.BalenaInvalidDeviceType(
"Device type #{options.deviceType} is incompatible with application #{options.application}"
)
manifestPromise.get('options')
.then (formOptions) ->
# Pass params as an override: if there is any param with exactly the same name as a
# required option, that value is used (and the corresponding question is not asked)
form.run(formOptions, override: options)
.then (answers) ->
answers.version = options.version
if resource.uuid?
generateDeviceConfig(resource, options.deviceApiKey || options['generate-device-api-key'], answers)
else
answers.deviceType = deviceType
generateApplicationConfig(resource, answers)
.then (config) ->
if options.output?
return writeFileAsync(options.output, JSON.stringify(config))
console.log(prettyjson.render(config))
.nodeify(done)

417
lib/actions/config.js Normal file
View File

@ -0,0 +1,417 @@
/*
Copyright 2016-2020 Balena Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as commandOptions from './command-options';
import { normalizeUuidProp } from '../utils/normalization';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
export const read = {
signature: 'config read',
description: 'read a device configuration',
help: `\
Use this command to read the config.json file from the mounted filesystem (e.g. SD card) of a provisioned device"
Examples:
$ balena config read --type raspberry-pi
$ balena config read --type raspberry-pi --drive /dev/disk2\
`,
options: [
{
signature: 'type',
description:
'device type (Check available types with `balena devices supported`)',
parameter: 'type',
alias: 't',
required: 'You have to specify a device type',
},
{
signature: 'drive',
description: 'drive',
parameter: 'drive',
alias: 'd',
},
],
permission: 'user',
root: true,
action(_params, options) {
const Promise = require('bluebird');
const config = require('balena-config-json');
const umountAsync = Promise.promisify(require('umount').umount);
const prettyjson = require('prettyjson');
return Promise.try(
() => options.drive || getVisuals().drive('Select the device drive'),
)
.tap(umountAsync)
.then((drive) => config.read(drive, options.type))
.tap((configJSON) => {
console.info(prettyjson.render(configJSON));
});
},
};
export const write = {
signature: 'config write <key> <value>',
description: 'write a device configuration',
help: `\
Use this command to write the config.json file to the mounted filesystem (e.g. SD card) of a provisioned device
Examples:
$ balena config write --type raspberry-pi username johndoe
$ balena config write --type raspberry-pi --drive /dev/disk2 username johndoe
$ balena config write --type raspberry-pi files.network/settings "..."\
`,
options: [
{
signature: 'type',
description:
'device type (Check available types with `balena devices supported`)',
parameter: 'type',
alias: 't',
required: 'You have to specify a device type',
},
{
signature: 'drive',
description: 'drive',
parameter: 'drive',
alias: 'd',
},
],
permission: 'user',
root: true,
action(params, options) {
const Promise = require('bluebird');
const _ = require('lodash');
const config = require('balena-config-json');
const umountAsync = Promise.promisify(require('umount').umount);
return Promise.try(
() => options.drive || getVisuals().drive('Select the device drive'),
)
.tap(umountAsync)
.then((drive) =>
config
.read(drive, options.type)
.then(function (configJSON) {
console.info(`Setting ${params.key} to ${params.value}`);
_.set(configJSON, params.key, params.value);
return configJSON;
})
.tap(() => umountAsync(drive))
.then((configJSON) => config.write(drive, options.type, configJSON)),
)
.tap(() => {
console.info('Done');
});
},
};
export const inject = {
signature: 'config inject <file>',
description: 'inject a device configuration file',
help: `\
Use this command to inject a config.json file to the mounted filesystem
(e.g. SD card or mounted balenaOS image) of a provisioned device"
Examples:
$ balena config inject my/config.json --type raspberry-pi
$ balena config inject my/config.json --type raspberry-pi --drive /dev/disk2\
`,
options: [
{
signature: 'type',
description:
'device type (Check available types with `balena devices supported`)',
parameter: 'type',
alias: 't',
required: 'You have to specify a device type',
},
{
signature: 'drive',
description: 'drive',
parameter: 'drive',
alias: 'd',
},
],
permission: 'user',
root: true,
action(params, options) {
const Promise = require('bluebird');
const util = require('util');
const config = require('balena-config-json');
const umountAsync = Promise.promisify(require('umount').umount);
const readFileAsync = util.promisify(require('fs').readFile);
return Promise.try(
() => options.drive || getVisuals().drive('Select the device drive'),
)
.tap(umountAsync)
.then((drive) =>
readFileAsync(params.file, 'utf8')
.then(JSON.parse)
.then((configJSON) => config.write(drive, options.type, configJSON)),
)
.tap(() => {
console.info('Done');
});
},
};
export const reconfigure = {
signature: 'config reconfigure',
description: 'reconfigure a provisioned device',
help: `\
Use this command to reconfigure a provisioned device
Examples:
$ balena config reconfigure --type raspberry-pi
$ balena config reconfigure --type raspberry-pi --advanced
$ balena config reconfigure --type raspberry-pi --drive /dev/disk2\
`,
options: [
{
signature: 'type',
description:
'device type (Check available types with `balena devices supported`)',
parameter: 'type',
alias: 't',
required: 'You have to specify a device type',
},
{
signature: 'drive',
description: 'drive',
parameter: 'drive',
alias: 'd',
},
{
signature: 'advanced',
description: 'show advanced commands',
boolean: true,
alias: 'v',
},
],
permission: 'user',
root: true,
action(_params, options) {
const Promise = require('bluebird');
const config = require('balena-config-json');
const { runCommand } = require('../utils/helpers');
const umountAsync = Promise.promisify(require('umount').umount);
return Promise.try(
() => options.drive || getVisuals().drive('Select the device drive'),
)
.tap(umountAsync)
.then((drive) =>
config
.read(drive, options.type)
.get('uuid')
.tap(() => umountAsync(drive))
.then(function (uuid) {
let configureCommand = `os configure ${drive} --device ${uuid}`;
if (options.advanced) {
configureCommand += ' --advanced';
}
return runCommand(configureCommand);
}),
)
.then(() => {
console.info('Done');
});
},
};
export const generate = {
signature: 'config generate',
description: 'generate a config.json file',
help: `\
Use this command to generate a config.json for a device or application.
Calling this command with the exact version number of the targeted image is required.
This is interactive by default, but you can do this automatically without interactivity
by specifying an option for each question on the command line, if you know the questions
that will be asked for the relevant device type.
In case that you want to configure an image for an application with mixed device types,
you can pass the --device-type argument along with --app to specify the target device type.
Examples:
$ balena config generate --device 7cf02a6 --version 2.12.7
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
$ balena config generate --app MyApp --version 2.12.7
$ balena config generate --app MyApp --version 2.12.7 --device-type fincm3
$ balena config generate --app MyApp --version 2.12.7 --output config.json
$ balena config generate --app MyApp --version 2.12.7 \
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1\
`,
options: [
commandOptions.osVersion,
commandOptions.optionalApplication,
commandOptions.optionalDevice,
commandOptions.optionalDeviceApiKey,
commandOptions.optionalDeviceType,
{
signature: 'generate-device-api-key',
description: 'generate a fresh device key for the device',
boolean: true,
},
{
signature: 'output',
description: 'output',
parameter: 'output',
alias: 'o',
},
// Options for non-interactive configuration
{
signature: 'network',
description: 'the network type to use: ethernet or wifi',
parameter: 'network',
},
{
signature: 'wifiSsid',
description:
'the wifi ssid to use (used only if --network is set to wifi)',
parameter: 'wifiSsid',
},
{
signature: 'wifiKey',
description:
'the wifi key to use (used only if --network is set to wifi)',
parameter: 'wifiKey',
},
{
signature: 'appUpdatePollInterval',
description:
'how frequently (in minutes) to poll for application updates',
parameter: 'appUpdatePollInterval',
},
],
permission: 'user',
action(_params, options) {
normalizeUuidProp(options, 'device');
const Promise = require('bluebird');
const writeFileAsync = Promise.promisify(require('fs').writeFile);
const balena = getBalenaSdk();
const form = require('resin-cli-form');
const prettyjson = require('prettyjson');
const {
generateDeviceConfig,
generateApplicationConfig,
} = require('../utils/config');
const helpers = require('../utils/helpers');
const { exitWithExpectedError } = require('../errors');
if (options.device == null && options.application == null) {
exitWithExpectedError(`\
You have to pass either a device or an application.
See the help page for examples:
$ balena help config generate\
`);
}
if (!options.application && options.deviceType) {
exitWithExpectedError(`\
Specifying a different device type is only supported when
generating a config for an application:
* An application, with --app <appname>
* A specific device type, with --device-type <deviceTypeSlug>
See the help page for examples:
$ balena help config generate\
`);
}
return Promise.try(
/** @returns {Promise<any>} */ function () {
if (options.device != null) {
return balena.models.device.get(options.device);
}
return balena.models.application.get(options.application);
},
)
.then(function (resource) {
const deviceType = options.deviceType || resource.device_type;
let manifestPromise = balena.models.device.getManifestBySlug(
deviceType,
);
if (options.application && options.deviceType) {
const app = resource;
const appManifestPromise = balena.models.device.getManifestBySlug(
app.device_type,
);
manifestPromise = manifestPromise.tap((paramDeviceType) =>
appManifestPromise.then(function (appDeviceType) {
if (
!helpers.areDeviceTypesCompatible(
appDeviceType,
paramDeviceType,
)
) {
throw new balena.errors.BalenaInvalidDeviceType(
`Device type ${options.deviceType} is incompatible with application ${options.application}`,
);
}
}),
);
}
return manifestPromise
.get('options')
.then((
formOptions, // Pass params as an override: if there is any param with exactly the same name as a
) =>
// required option, that value is used (and the corresponding question is not asked)
form.run(formOptions, { override: options }),
)
.then(function (answers) {
answers.version = options.version;
if (resource.uuid != null) {
return generateDeviceConfig(
resource,
options.deviceApiKey || options['generate-device-api-key'],
answers,
);
} else {
answers.deviceType = deviceType;
return generateApplicationConfig(resource, answers);
}
});
})
.then(function (config) {
if (options.output != null) {
return writeFileAsync(options.output, JSON.stringify(config));
}
console.log(prettyjson.render(config));
});
},
};

View File

@ -1,234 +0,0 @@
# Imported here because it's needed for the setup
# of this action
Promise = require('bluebird')
dockerUtils = require('../utils/docker')
compose = require('../utils/compose')
{ registrySecretsHelp } = require('../utils/messages')
###
Opts must be an object with the following keys:
app: the application instance to deploy to
image: the image to deploy; optional
dockerfilePath: name of an alternative Dockerfile; optional
shouldPerformBuild
shouldUploadLogs
buildEmulated
buildOpts: arguments to forward to docker build command
###
deployProject = (docker, logger, composeOpts, opts) ->
_ = require('lodash')
doodles = require('resin-doodles')
sdk = require('balena-sdk').fromSharedOptions()
compose.loadProject(
logger
composeOpts.projectPath
composeOpts.projectName
opts.image
composeOpts.dockerfilePath # ok if undefined
)
.then (project) ->
if project.descriptors.length > 1 and !opts.app.application_type?[0]?.supports_multicontainer
throw new Error('Target application does not support multiple containers. Aborting!')
# find which services use images that already exist locally
Promise.map project.descriptors, (d) ->
# unconditionally build (or pull) if explicitly requested
return d if opts.shouldPerformBuild
docker.getImage(d.image.tag ? d.image).inspect()
.return(d.serviceName)
.catchReturn()
.filter (d) -> !!d
.then (servicesToSkip) ->
# multibuild takes in a composition and always attempts to
# build or pull all services. we workaround that here by
# passing a modified composition.
compositionToBuild = _.cloneDeep(project.composition)
compositionToBuild.services = _.omit(compositionToBuild.services, servicesToSkip)
if _.size(compositionToBuild.services) is 0
logger.logInfo('Everything is up to date (use --build to force a rebuild)')
return {}
compose.buildProject(
docker
logger
project.path
project.name
compositionToBuild
opts.app.arch
opts.app.device_type
opts.buildEmulated
opts.buildOpts
composeOpts.inlineLogs
)
.then (builtImages) ->
_.keyBy(builtImages, 'serviceName')
.then (builtImages) ->
project.descriptors.map (d) ->
builtImages[d.serviceName] ? {
serviceName: d.serviceName,
name: d.image.tag ? d.image
logs: 'Build skipped; image for service already exists.'
props: {}
}
.then (images) ->
if opts.app.application_type?[0]?.is_legacy
chalk = require('chalk')
legacyDeploy = require('../utils/deploy-legacy')
msg = chalk.yellow('Target application requires legacy deploy method.')
logger.logWarn(msg)
return Promise.join(
docker
logger
sdk.auth.getToken()
sdk.auth.whoami()
sdk.settings.get('balenaUrl')
{
# opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
appName: opts.appName
imageName: images[0].name
buildLogs: images[0].logs
shouldUploadLogs: opts.shouldUploadLogs
}
legacyDeploy
)
.then (releaseId) ->
sdk.models.release.get(releaseId, $select: [ 'commit' ])
Promise.join(
sdk.auth.getUserId()
sdk.auth.getToken()
sdk.settings.get('apiUrl')
(userId, auth, apiEndpoint) ->
compose.deployProject(
docker
logger
project.composition
images
opts.app.id
userId
"Bearer #{auth}"
apiEndpoint
!opts.shouldUploadLogs
)
)
.then (release) ->
logger.logSuccess('Deploy succeeded!')
logger.logSuccess("Release: #{release.commit}")
console.log()
console.log(doodles.getDoodle()) # Show charlie
console.log()
.tapCatch (e) ->
logger.logError('Deploy failed')
module.exports =
signature: 'deploy <appName> [image]'
description: 'Deploy a single image or a multicontainer project to a balena application'
help: """
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
Use this command to deploy an image or a complete multicontainer project to an
application, optionally building it first. The source images are searched for
(and optionally built) using the docker daemon in your development machine or
balena device. (See also the `balena push` command for the option of building
the image in the balenaCloud build servers.)
Unless an image is specified, this command will look into the current directory
(or the one specified by --source) for a docker-compose.yml file. If one is
found, this command will deploy each service defined in the compose file,
building it first if an image for it doesn't exist. If a compose file isn't
found, the command will look for a Dockerfile[.template] file (or alternative
Dockerfile specified with the `-f` option), and if yet that isn't found, it
will try to generate one.
To deploy to an app on which you're a collaborator, use
`balena deploy <appOwnerUsername>/<appName>`.
When --build is used, all options supported by `balena build` are also supported
by this command.
#{registrySecretsHelp}
Examples:
$ balena deploy myApp
$ balena deploy myApp --build --source myBuildDir/
$ balena deploy myApp myApp/myImage
"""
permission: 'user'
primary: true
options: dockerUtils.appendOptions compose.appendOptions [
{
signature: 'source'
parameter: 'source'
description: 'Specify an alternate source directory; default is the working directory'
alias: 's'
},
{
signature: 'build'
boolean: true
description: 'Force a rebuild before deploy'
alias: 'b'
},
{
signature: 'nologupload'
description: "Don't upload build logs to the dashboard with image (if building)"
boolean: true
}
]
action: (params, options, done) ->
# compositions with many services trigger misleading warnings
require('events').defaultMaxListeners = 1000
sdk = (require('balena-sdk')).fromSharedOptions()
{ validateComposeOptions } = require('../utils/compose_ts')
helpers = require('../utils/helpers')
Logger = require('../utils/logger')
logger = Logger.getLogger()
logger.logDebug('Parsing input...')
# when Capitano converts a positional parameter (but not an option)
# to a number, the original value is preserved with the _raw suffix
{ appName, appName_raw, image } = params
# look into "balena build" options if appName isn't given
appName = appName_raw || appName || options.application
delete options.application
Promise.resolve(validateComposeOptions(sdk, options))
.then ->
if not appName?
throw new Error('Please specify the name of the application to deploy')
if image? and options.build
throw new Error('Build option is not applicable when specifying an image')
Promise.join(
helpers.getApplication(appName)
helpers.getArchAndDeviceType(appName)
(app, { arch, device_type }) ->
app.arch = arch
app.device_type = device_type
return app
)
.then (app) ->
[ app, image, !!options.build, !options.nologupload ]
.then ([ app, image, shouldPerformBuild, shouldUploadLogs ]) ->
Promise.join(
dockerUtils.getDocker(options)
dockerUtils.generateBuildOpts(options)
compose.generateOpts(options)
(docker, buildOpts, composeOpts) ->
deployProject(docker, logger, composeOpts, {
app
appName # may be prefixed by 'owner/', unlike app.app_name
image
shouldPerformBuild
shouldUploadLogs
buildEmulated: !!options.emulated
buildOpts
})
)
.asCallback(done)

317
lib/actions/deploy.js Normal file
View File

@ -0,0 +1,317 @@
/**
* @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.
*/
// Imported here because it's needed for the setup
// of this action
import * as Promise from 'bluebird';
import * as dockerUtils from '../utils/docker';
import * as compose from '../utils/compose';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import { ExpectedError } from '../errors';
import { getBalenaSdk, getChalk } from '../utils/lazy';
/*
Opts must be an object with the following keys:
app: the application instance to deploy to
image: the image to deploy; optional
dockerfilePath: name of an alternative Dockerfile; optional
shouldPerformBuild
shouldUploadLogs
buildEmulated
buildOpts: arguments to forward to docker build command
*/
const deployProject = function (docker, logger, composeOpts, opts) {
const _ = require('lodash');
const doodles = require('resin-doodles');
const sdk = getBalenaSdk();
const {
deployProject: $deployProject,
loadProject,
} = require('../utils/compose_ts');
return Promise.resolve(loadProject(logger, composeOpts, opts.image))
.then(function (project) {
if (
project.descriptors.length > 1 &&
!opts.app.application_type?.[0]?.supports_multicontainer
) {
throw new Error(
'Target application does not support multiple containers. Aborting!',
);
}
// find which services use images that already exist locally
return (
Promise.map(project.descriptors, function (d) {
// unconditionally build (or pull) if explicitly requested
if (opts.shouldPerformBuild) {
return d;
}
return docker
.getImage(typeof d.image === 'string' ? d.image : d.image.tag)
.inspect()
.return(d.serviceName)
.catchReturn();
})
.filter((d) => !!d)
.then(function (servicesToSkip) {
// multibuild takes in a composition and always attempts to
// build or pull all services. we workaround that here by
// passing a modified composition.
const compositionToBuild = _.cloneDeep(project.composition);
compositionToBuild.services = _.omit(
compositionToBuild.services,
servicesToSkip,
);
if (_.size(compositionToBuild.services) === 0) {
logger.logInfo(
'Everything is up to date (use --build to force a rebuild)',
);
return {};
}
return compose
.buildProject(
docker,
logger,
project.path,
project.name,
compositionToBuild,
opts.app.arch,
opts.app.device_type,
opts.buildEmulated,
opts.buildOpts,
composeOpts.inlineLogs,
composeOpts.convertEol,
composeOpts.dockerfilePath,
composeOpts.nogitignore,
)
.then((builtImages) => _.keyBy(builtImages, 'serviceName'));
})
.then((builtImages) =>
project.descriptors.map(
(d) =>
builtImages[d.serviceName] ?? {
serviceName: d.serviceName,
name: typeof d.image === 'string' ? d.image : d.image.tag,
logs: 'Build skipped; image for service already exists.',
props: {},
},
),
)
// @ts-ignore slightly different return types of partial vs non-partial release
.then(function (images) {
if (opts.app.application_type?.[0]?.is_legacy) {
const { deployLegacy } = require('../utils/deploy-legacy');
const msg = getChalk().yellow(
'Target application requires legacy deploy method.',
);
logger.logWarn(msg);
return Promise.join(
docker,
logger,
sdk.auth.getToken(),
sdk.auth.whoami(),
sdk.settings.get('balenaUrl'),
{
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
appName: opts.appName,
imageName: images[0].name,
buildLogs: images[0].logs,
shouldUploadLogs: opts.shouldUploadLogs,
},
deployLegacy,
).then((releaseId) =>
// @ts-ignore releaseId should be inferred as a number because that's what deployLegacy is
// typed as returning but the .js type-checking doesn't manage to infer it correctly due to
// Promise.join typings
sdk.models.release.get(releaseId, { $select: ['commit'] }),
);
}
return Promise.join(
sdk.auth.getUserId(),
sdk.auth.getToken(),
sdk.settings.get('apiUrl'),
(userId, auth, apiEndpoint) =>
$deployProject(
docker,
logger,
project.composition,
images,
opts.app.id,
userId,
`Bearer ${auth}`,
apiEndpoint,
!opts.shouldUploadLogs,
),
);
})
);
})
.then(function (release) {
logger.outputDeferredMessages();
logger.logSuccess('Deploy succeeded!');
logger.logSuccess(`Release: ${release.commit}`);
console.log();
console.log(doodles.getDoodle()); // Show charlie
console.log();
})
.tapCatch(() => {
logger.logError('Deploy failed');
});
};
export const deploy = {
signature: 'deploy <appName> [image]',
description:
'Deploy a single image or a multicontainer project to a balena application',
help: `\
Usage: \`deploy <appName> ([image] | --build [--source build-dir])\`
Use this command to deploy an image or a complete multicontainer project to an
application, optionally building it first. The source images are searched for
(and optionally built) using the docker daemon in your development machine or
balena device. (See also the \`balena push\` command for the option of building
the image in the balenaCloud build servers.)
Unless an image is specified, this command will look into the current directory
(or the one specified by --source) for a docker-compose.yml file. If one is
found, this command will deploy each service defined in the compose file,
building it first if an image for it doesn't exist. If a compose file isn't
found, the command will look for a Dockerfile[.template] file (or alternative
Dockerfile specified with the \`-f\` option), and if yet that isn't found, it
will try to generate one.
To deploy to an app on which you're a collaborator, use
\`balena deploy <appOwnerUsername>/<appName>\`.
When --build is used, all options supported by \`balena build\` are also supported
by this command.
${registrySecretsHelp}
${dockerignoreHelp}
Examples:
$ balena deploy myApp
$ balena deploy myApp --build --source myBuildDir/
$ balena deploy myApp myApp/myImage\
`,
permission: 'user',
primary: true,
options: dockerUtils.appendOptions(
compose.appendOptions([
{
signature: 'source',
parameter: 'source',
description:
'Specify an alternate source directory; default is the working directory',
alias: 's',
},
{
signature: 'build',
boolean: true,
description: 'Force a rebuild before deploy',
alias: 'b',
},
{
signature: 'nologupload',
description:
"Don't upload build logs to the dashboard with image (if building)",
boolean: true,
},
]),
),
action(params, options) {
// compositions with many services trigger misleading warnings
// @ts-ignore editing property that isn't typed but does exist
require('events').defaultMaxListeners = 1000;
const sdk = getBalenaSdk();
const {
getRegistrySecrets,
validateProjectDirectory,
} = require('../utils/compose_ts');
const helpers = require('../utils/helpers');
const Logger = require('../utils/logger');
const logger = Logger.getLogger();
logger.logDebug('Parsing input...');
// when Capitano converts a positional parameter (but not an option)
// to a number, the original value is preserved with the _raw suffix
let { appName, appName_raw, image } = params;
// look into "balena build" options if appName isn't given
appName = appName_raw || appName || options.application;
delete options.application;
return Promise.try(function () {
if (appName == null) {
throw new ExpectedError(
'Please specify the name of the application to deploy',
);
}
if (image != null && options.build) {
throw new ExpectedError(
'Build option is not applicable when specifying an image',
);
}
})
.then(function () {
if (image) {
return getRegistrySecrets(sdk, options['registry-secrets']).then(
(registrySecrets) => {
options['registry-secrets'] = registrySecrets;
},
);
} else {
return validateProjectDirectory(sdk, {
dockerfilePath: options.dockerfile,
noParentCheck: options['noparent-check'] || false,
projectPath: options.source || '.',
registrySecretsPath: options['registry-secrets'],
}).then(function ({ dockerfilePath, registrySecrets }) {
options.dockerfile = dockerfilePath;
options['registry-secrets'] = registrySecrets;
});
}
})
.then(() => helpers.getAppWithArch(appName))
.then(function (app) {
return Promise.join(
dockerUtils.getDocker(options),
dockerUtils.generateBuildOpts(options),
compose.generateOpts(options),
(docker, buildOpts, composeOpts) =>
deployProject(docker, logger, composeOpts, {
app,
appName, // may be prefixed by 'owner/', unlike app.app_name
image,
shouldPerformBuild: !!options.build,
shouldUploadLogs: !options.nologupload,
buildEmulated: !!options.emulated,
buildOpts,
}),
);
});
},
};

View File

@ -1,451 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
commandOptions = require('./command-options')
_ = require('lodash')
{ normalizeUuidProp } = require('../utils/normalization')
expandForAppName = {
$expand: belongs_to__application: $select: 'app_name'
}
exports.list =
signature: 'devices'
description: 'list all devices'
help: '''
Use this command to list all devices that belong to you.
You can filter the devices by application by using the `--application` option.
Examples:
$ balena devices
$ balena devices --application MyApp
$ balena devices --app MyApp
$ balena devices -a MyApp
'''
options: [ commandOptions.optionalApplication ]
permission: 'user'
primary: true
action: (params, options, done) ->
Promise = require('bluebird')
balena = require('balena-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
Promise.try ->
if options.application?
return balena.models.device.getAllByApplication(options.application, expandForAppName)
return balena.models.device.getAll(expandForAppName)
.tap (devices) ->
devices = _.map devices, (device) ->
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
device.application_name = device.belongs_to__application[0].app_name
device.uuid = device.uuid.slice(0, 7)
return device
console.log visuals.table.horizontal devices, [
'id'
'uuid'
'device_name'
'device_type'
'application_name'
'status'
'is_online'
'supervisor_version'
'os_version'
'dashboard_url'
]
.nodeify(done)
exports.info =
signature: 'device <uuid>'
description: 'list a single device'
help: '''
Use this command to show information about a single device.
Examples:
$ balena device 7cf02a6
'''
permission: 'user'
primary: true
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
balena.models.device.get(params.uuid, expandForAppName)
.then (device) ->
balena.models.device.getStatus(device).then (status) ->
device.status = status
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
device.application_name = device.belongs_to__application[0].app_name
device.commit = device.is_on__commit
console.log visuals.table.vertical device, [
"$#{device.device_name}$"
'id'
'device_type'
'status'
'is_online'
'ip_address'
'application_name'
'last_seen'
'uuid'
'commit'
'supervisor_version'
'is_web_accessible'
'note'
'os_version'
'dashboard_url'
]
.nodeify(done)
exports.register =
signature: 'device register <application>'
description: 'register a device'
help: '''
Use this command to register a device to an application.
Examples:
$ balena device register MyApp
$ balena device register MyApp --uuid <uuid>
'''
permission: 'user'
options: [
{
signature: 'uuid'
description: 'custom uuid'
parameter: 'uuid'
alias: 'u'
}
]
action: (params, options, done) ->
Promise = require('bluebird')
balena = require('balena-sdk').fromSharedOptions()
Promise.join(
balena.models.application.get(params.application)
options.uuid ? balena.models.device.generateUniqueKey()
(application, uuid) ->
console.info("Registering to #{application.app_name}: #{uuid}")
return balena.models.device.register(application.id, uuid)
)
.get('uuid')
.nodeify(done)
exports.remove =
signature: 'device rm <uuid>'
description: 'remove a device'
help: '''
Use this command to remove a device from balena.
Notice this command asks for confirmation interactively.
You can avoid this by passing the `--yes` boolean option.
Examples:
$ balena device rm 7cf02a6
$ balena device rm 7cf02a6 --yes
'''
options: [ commandOptions.yes ]
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
patterns.confirm(options.yes, 'Are you sure you want to delete the device?').then ->
balena.models.device.remove(params.uuid)
.nodeify(done)
exports.identify =
signature: 'device identify <uuid>'
description: 'identify a device with a UUID'
help: '''
Use this command to identify a device.
In the Raspberry Pi, the ACT led is blinked several times.
Examples:
$ balena device identify 23c73a1
'''
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
balena.models.device.identify(params.uuid).nodeify(done)
exports.reboot =
signature: 'device reboot <uuid>'
description: 'restart a device'
help: '''
Use this command to remotely reboot a device
Examples:
$ balena device reboot 23c73a1
'''
options: [ commandOptions.forceUpdateLock ]
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
balena.models.device.reboot(params.uuid, options).nodeify(done)
exports.shutdown =
signature: 'device shutdown <uuid>'
description: 'shutdown a device'
help: '''
Use this command to remotely shutdown a device
Examples:
$ balena device shutdown 23c73a1
'''
options: [ commandOptions.forceUpdateLock ]
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
balena.models.device.shutdown(params.uuid, options).nodeify(done)
exports.enableDeviceUrl =
signature: 'device public-url enable <uuid>'
description: 'enable public URL for a device'
help: '''
Use this command to enable public URL for a device
Examples:
$ balena device public-url enable 23c73a1
'''
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
balena.models.device.enableDeviceUrl(params.uuid).nodeify(done)
exports.disableDeviceUrl =
signature: 'device public-url disable <uuid>'
description: 'disable public URL for a device'
help: '''
Use this command to disable public URL for a device
Examples:
$ balena device public-url disable 23c73a1
'''
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
balena.models.device.disableDeviceUrl(params.uuid).nodeify(done)
exports.getDeviceUrl =
signature: 'device public-url <uuid>'
description: 'gets the public URL of a device'
help: '''
Use this command to get the public URL of a device
Examples:
$ balena device public-url 23c73a1
'''
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
balena.models.device.getDeviceUrl(params.uuid).then (url) ->
console.log(url)
.nodeify(done)
exports.hasDeviceUrl =
signature: 'device public-url status <uuid>'
description: 'Returns true if public URL is enabled for a device'
help: '''
Use this command to determine if public URL is enabled for a device
Examples:
$ balena device public-url status 23c73a1
'''
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
balena.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
console.log(hasDeviceUrl)
.nodeify(done)
exports.rename =
signature: 'device rename <uuid> [newName]'
description: 'rename a balena device'
help: '''
Use this command to rename a device.
If you omit the name, you'll get asked for it interactively.
Examples:
$ balena device rename 7cf02a6
$ balena device rename 7cf02a6 MyPi
'''
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
Promise = require('bluebird')
balena = require('balena-sdk').fromSharedOptions()
form = require('resin-cli-form')
Promise.try ->
return params.newName if not _.isEmpty(params.newName)
form.ask
message: 'How do you want to name this device?'
type: 'input'
.then(_.partial(balena.models.device.rename, params.uuid))
.nodeify(done)
exports.move =
signature: 'device move <uuid>'
description: 'move a device to another application'
help: '''
Use this command to move a device to another application you own.
If you omit the application, you'll get asked for it interactively.
Examples:
$ balena device move 7cf02a6
$ balena device move 7cf02a6 --application MyNewApp
'''
permission: 'user'
options: [ commandOptions.optionalApplication ]
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
balena.models.device.get(params.uuid, expandForAppName).then (device) ->
return options.application if options.application
return Promise.all([
balena.models.device.getManifestBySlug(device.device_type)
balena.models.config.getDeviceTypes()
]).then ([deviceDeviceType, deviceTypes]) ->
compatibleDeviceTypes = deviceTypes.filter (dt) ->
balena.models.os.isArchitectureCompatibleWith(deviceDeviceType.arch, dt.arch) &&
!!dt.isDependent == !!deviceDeviceType.isDependent &&
dt.state != 'DISCONTINUED'
return patterns.selectApplication (application) ->
return _.every [
_.some(compatibleDeviceTypes, (dt) -> dt.slug == application.device_type)
device.belongs_to__application[0].app_name isnt application.app_name
]
.tap (application) ->
return balena.models.device.move(params.uuid, application)
.then (application) ->
console.info("#{params.uuid} was moved to #{application}")
.nodeify(done)
exports.init =
signature: 'device init'
description: 'initialise a device with balenaOS'
help: '''
Use this command to download the OS image of a certain application and write it to an SD Card.
Notice this command may ask for confirmation interactively.
You can avoid this by passing the `--yes` boolean option.
Examples:
$ balena device init
$ balena device init --application MyApp
'''
options: [
commandOptions.optionalApplication
commandOptions.yes
commandOptions.advancedConfig
_.assign({}, commandOptions.osVersionOrSemver, { signature: 'os-version', parameter: 'os-version' })
commandOptions.drive
{
signature: 'config'
description: 'path to the config JSON file, see `balena os build-config`'
parameter: 'config'
}
]
permission: 'user'
action: (params, options, done) ->
Promise = require('bluebird')
rimraf = Promise.promisify(require('rimraf'))
tmp = require('tmp')
tmpNameAsync = Promise.promisify(tmp.tmpName)
tmp.setGracefulCleanup()
balena = require('balena-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
{ runCommand } = require('../utils/helpers')
Promise.try ->
return options.application if options.application?
return patterns.selectApplication()
.then(balena.models.application.get)
.then (application) ->
download = ->
tmpNameAsync().then (tempPath) ->
osVersion = options['os-version'] or 'default'
runCommand("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
.disposer (tempPath) ->
return rimraf(tempPath)
Promise.using download(), (tempPath) ->
runCommand("device register #{application.app_name}")
.then(balena.models.device.get)
.tap (device) ->
configureCommand = "os configure '#{tempPath}' --device #{device.uuid}"
if options.config
configureCommand += " --config '#{options.config}'"
else if options.advanced
configureCommand += ' --advanced'
runCommand(configureCommand)
.then ->
osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
if options.yes
osInitCommand += ' --yes'
if options.drive
osInitCommand += " --drive #{options.drive}"
runCommand(osInitCommand)
# Make sure the device resource is removed if there is an
# error when configuring or initializing a device image
.catch (error) ->
balena.models.device.remove(device.uuid).finally ->
throw error
.then (device) ->
console.log('Done')
return device.uuid
.nodeify(done)
tsActions = require('./device_ts')
exports.osUpdate = tsActions.osUpdate
exports.supported = tsActions.supported

460
lib/actions/device.js Normal file
View File

@ -0,0 +1,460 @@
/*
Copyright 2016-2020 Balena Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as commandOptions from './command-options';
import * as _ from 'lodash';
import { normalizeUuidProp } from '../utils/normalization';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
/** @type {import('balena-sdk').PineOptionsFor<import('balena-sdk').Device>} */
const expandForAppName = {
$expand: { belongs_to__application: { $select: 'app_name' } },
};
export const list = {
signature: 'devices',
description: 'list all devices',
help: `\
Use this command to list all devices that belong to you.
You can filter the devices by application by using the \`--application\` option.
Examples:
$ balena devices
$ balena devices --application MyApp
$ balena devices --app MyApp
$ balena devices -a MyApp\
`,
options: [commandOptions.optionalApplication],
permission: 'user',
primary: true,
action(_params, options) {
const Promise = require('bluebird');
const balena = getBalenaSdk();
return Promise.try(function () {
if (options.application != null) {
return balena.models.device.getAllByApplication(
options.application,
expandForAppName,
);
}
return balena.models.device.getAll(expandForAppName);
}).tap(function (devices) {
devices = _.map(devices, function (device) {
// @ts-ignore extending the device object with extra props
device.dashboard_url = balena.models.device.getDashboardUrl(
device.uuid,
);
// @ts-ignore extending the device object with extra props
device.application_name = device.belongs_to__application?.[0]
? device.belongs_to__application[0].app_name
: 'N/a';
device.uuid = device.uuid.slice(0, 7);
return device;
});
console.log(
getVisuals().table.horizontal(devices, [
'id',
'uuid',
'device_name',
'device_type',
'application_name',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
]),
);
});
},
};
export const info = {
signature: 'device <uuid>',
description: 'list a single device',
help: `\
Use this command to show information about a single device.
Examples:
$ balena device 7cf02a6\
`,
permission: 'user',
primary: true,
action(params) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
return balena.models.device
.get(params.uuid, expandForAppName)
.then((device) =>
// @ts-ignore `device.getStatus` requires a device with service info, but
// this device isn't typed with them, possibly needs fixing?
balena.models.device.getStatus(params.uuid).then(function (status) {
device.status = status;
// @ts-ignore extending the device object with extra props
device.dashboard_url = balena.models.device.getDashboardUrl(
device.uuid,
);
// @ts-ignore extending the device object with extra props
device.application_name = device.belongs_to__application?.[0]
? device.belongs_to__application[0].app_name
: 'N/a';
// @ts-ignore extending the device object with extra props
device.commit = device.is_on__commit;
console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
'id',
'device_type',
'status',
'is_online',
'ip_address',
'mac_address',
'application_name',
'last_seen',
'uuid',
'commit',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'dashboard_url',
]),
);
}),
);
},
};
export const register = {
signature: 'device register <application>',
description: 'register a device',
help: `\
Use this command to register a device to an application.
Examples:
$ balena device register MyApp
$ balena device register MyApp --uuid <uuid>\
`,
permission: 'user',
options: [
{
signature: 'uuid',
description: 'custom uuid',
parameter: 'uuid',
alias: 'u',
},
],
action(params, options) {
const Promise = require('bluebird');
const balena = getBalenaSdk();
return Promise.join(
balena.models.application.get(params.application),
options.uuid ?? balena.models.device.generateUniqueKey(),
function (application, uuid) {
console.info(`Registering to ${application.app_name}: ${uuid}`);
return balena.models.device.register(application.id, uuid);
},
).get('uuid');
},
};
export const remove = {
signature: 'device rm <uuid>',
description: 'remove a device',
help: `\
Use this command to remove a device from balena.
Notice this command asks for confirmation interactively.
You can avoid this by passing the \`--yes\` boolean option.
Examples:
$ balena device rm 7cf02a6
$ balena device rm 7cf02a6 --yes\
`,
options: [commandOptions.yes],
permission: 'user',
action(params, options) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
const patterns = require('../utils/patterns');
return patterns
.confirm(options.yes, 'Are you sure you want to delete the device?')
.then(() => balena.models.device.remove(params.uuid));
},
};
export const identify = {
signature: 'device identify <uuid>',
description: 'identify a device with a UUID',
help: `\
Use this command to identify a device.
In the Raspberry Pi, the ACT led is blinked several times.
Examples:
$ balena device identify 23c73a1\
`,
permission: 'user',
action(params) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
return balena.models.device.identify(params.uuid);
},
};
export const reboot = {
signature: 'device reboot <uuid>',
description: 'restart a device',
help: `\
Use this command to remotely reboot a device
Examples:
$ balena device reboot 23c73a1\
`,
options: [commandOptions.forceUpdateLock],
permission: 'user',
action(params, options) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
return balena.models.device.reboot(params.uuid, options);
},
};
export const shutdown = {
signature: 'device shutdown <uuid>',
description: 'shutdown a device',
help: `\
Use this command to remotely shutdown a device
Examples:
$ balena device shutdown 23c73a1\
`,
options: [commandOptions.forceUpdateLock],
permission: 'user',
action(params, options) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
return balena.models.device.shutdown(params.uuid, options);
},
};
export const rename = {
signature: 'device rename <uuid> [newName]',
description: 'rename a balena device',
help: `\
Use this command to rename a device.
If you omit the name, you'll get asked for it interactively.
Examples:
$ balena device rename 7cf02a6
$ balena device rename 7cf02a6 MyPi\
`,
permission: 'user',
action(params) {
normalizeUuidProp(params);
const Promise = require('bluebird');
const balena = getBalenaSdk();
const form = require('resin-cli-form');
return Promise.try(function () {
if (!_.isEmpty(params.newName)) {
return params.newName;
}
return form.ask({
message: 'How do you want to name this device?',
type: 'input',
});
}).then(_.partial(balena.models.device.rename, params.uuid));
},
};
export const move = {
signature: 'device move <uuid>',
description: 'move a device to another application',
help: `\
Use this command to move a device to another application you own.
If you omit the application, you'll get asked for it interactively.
Examples:
$ balena device move 7cf02a6
$ balena device move 7cf02a6 --application MyNewApp\
`,
permission: 'user',
options: [commandOptions.optionalApplication],
action(params, options) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
const patterns = require('../utils/patterns');
return balena.models.device
.get(params.uuid, expandForAppName)
.then(function (device) {
// @ts-ignore extending the device object with extra props
device.application_name = device.belongs_to__application?.[0]
? device.belongs_to__application[0].app_name
: 'N/a';
if (options.application) {
return options.application;
}
return Promise.all([
balena.models.device.getManifestBySlug(device.device_type),
balena.models.config.getDeviceTypes(),
]).then(function ([deviceDeviceType, deviceTypes]) {
const compatibleDeviceTypes = deviceTypes.filter(
(dt) =>
balena.models.os.isArchitectureCompatibleWith(
deviceDeviceType.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceDeviceType.isDependent &&
dt.state !== 'DISCONTINUED',
);
return patterns.selectApplication((application) =>
_.every([
_.some(
compatibleDeviceTypes,
(dt) => dt.slug === application.device_type,
),
// @ts-ignore using the extended device object prop
device.application_name !== application.app_name,
]),
);
});
})
.tap((application) => balena.models.device.move(params.uuid, application))
.then((application) => {
console.info(`${params.uuid} was moved to ${application}`);
});
},
};
export const init = {
signature: 'device init',
description: 'initialise a device with balenaOS',
help: `\
Use this command to download the OS image of a certain application and write it to an SD Card.
Notice this command may ask for confirmation interactively.
You can avoid this by passing the \`--yes\` boolean option.
Examples:
$ balena device init
$ balena device init --application MyApp\
`,
options: [
commandOptions.optionalApplication,
commandOptions.yes,
commandOptions.advancedConfig,
_.assign({}, commandOptions.osVersionOrSemver, {
signature: 'os-version',
parameter: 'os-version',
}),
commandOptions.drive,
{
signature: 'config',
description: 'path to the config JSON file, see `balena os build-config`',
parameter: 'config',
},
],
permission: 'user',
action(_params, options) {
const Promise = require('bluebird');
const rimraf = Promise.promisify(require('rimraf'));
const tmp = require('tmp');
const tmpNameAsync = Promise.promisify(tmp.tmpName);
tmp.setGracefulCleanup();
const balena = getBalenaSdk();
const patterns = require('../utils/patterns');
const { runCommand } = require('../utils/helpers');
return Promise.try(function () {
if (options.application != null) {
return options.application;
}
return patterns.selectApplication();
})
.then(balena.models.application.get)
.then(function (application) {
const download = () =>
tmpNameAsync()
.then(function (tempPath) {
const osVersion = options['os-version'] || 'default';
return runCommand(
`os download ${application.device_type} --output '${tempPath}' --version ${osVersion}`,
);
})
.disposer((tempPath) => rimraf(tempPath));
return Promise.using(download(), (tempPath) =>
runCommand(`device register ${application.app_name}`)
.then(balena.models.device.get)
.tap(function (device) {
let configureCommand = `os configure '${tempPath}' --device ${device.uuid}`;
if (options.config) {
configureCommand += ` --config '${options.config}'`;
} else if (options.advanced) {
configureCommand += ' --advanced';
}
return runCommand(configureCommand)
.then(function () {
let osInitCommand = `os initialize '${tempPath}' --type ${application.device_type}`;
if (options.yes) {
osInitCommand += ' --yes';
}
if (options.drive) {
osInitCommand += ` --drive ${options.drive}`;
}
return runCommand(osInitCommand);
})
.catch((error) =>
balena.models.device.remove(device.uuid).finally(function () {
throw error;
}),
);
}),
).then(function (device) {
console.log('Done');
return device.uuid;
});
});
},
};
export { osUpdate } from './device_ts';

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2017 Balena
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.
@ -16,34 +16,11 @@ limitations under the License.
import { Device } from 'balena-sdk';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { ExpectedError } from '../errors';
import { getBalenaSdk } from '../utils/lazy';
import { normalizeUuidProp } from '../utils/normalization';
import * as commandOptions from './command-options';
export const supported: CommandDefinition<{}, {}> = {
signature: 'devices supported',
description: 'list all supported devices',
help: stripIndent`
Use this command to get the list of all supported devices.
Examples:
$ balena devices supported
`,
async action(_params, _options) {
const sdk = (await import('balena-sdk')).fromSharedOptions();
const visuals = await import('resin-cli-visuals');
const _ = await import('lodash');
let deviceTypes = await sdk.models.config.getDeviceTypes();
const fields = ['slug', 'name'];
deviceTypes = _.sortBy(deviceTypes, fields).filter(
dt => dt.state !== 'DISCONTINUED',
);
const output = await visuals.table.horizontal(deviceTypes, fields);
console.log(output);
},
};
// tslint:disable-next-line:no-namespace
namespace OsUpdate {
export interface Args {
@ -63,6 +40,8 @@ export const osUpdate: CommandDefinition<OsUpdate.Args, OsUpdate.Options> = {
Notice this command will ask for confirmation interactively.
You can avoid this by passing the \`--yes\` boolean option.
Requires balenaCloud; will not work with openBalena or standalone balenaOS.
Examples:
$ balena device os-update 23c73a1
@ -70,71 +49,74 @@ export const osUpdate: CommandDefinition<OsUpdate.Args, OsUpdate.Options> = {
`,
options: [commandOptions.optionalOsVersion, commandOptions.yes],
permission: 'user',
async action(params, options, done) {
async action(params, options) {
normalizeUuidProp(params);
const balena = await import('balena-sdk');
const _ = await import('lodash');
const sdk = balena.fromSharedOptions();
const sdk = getBalenaSdk();
const patterns = await import('../utils/patterns');
const form = await import('resin-cli-form');
return sdk.models.device
.get(params.uuid, {
$select: ['uuid', 'device_type', 'os_version', 'os_variant'],
})
.then(async ({ uuid, device_type, os_version, os_variant }) => {
const currentOsVersion = sdk.models.device.getOsVersion({
os_version,
os_variant,
} as Device);
if (!currentOsVersion) {
patterns.exitWithExpectedError(
'The current os version of the device is not available',
);
// Just to make TS happy
return;
}
// Get device info
const {
uuid,
device_type,
os_version,
os_variant,
} = await sdk.models.device.get(params.uuid, {
$select: ['uuid', 'device_type', 'os_version', 'os_variant'],
});
return sdk.models.os
.getSupportedOsUpdateVersions(device_type, currentOsVersion)
.then(hupVersionInfo => {
if (hupVersionInfo.versions.length === 0) {
patterns.exitWithExpectedError(
'There are no available Host OS update targets for this device',
);
}
// Get current device OS version
const currentOsVersion = sdk.models.device.getOsVersion({
os_version,
os_variant,
} as Device);
if (!currentOsVersion) {
throw new ExpectedError(
'The current os version of the device is not available',
);
}
if (options.version != null) {
if (!_.includes(hupVersionInfo.versions, options.version)) {
patterns.exitWithExpectedError(
'The provided version is not in the Host OS update targets for this device',
);
}
return options.version;
}
// Get supported OS update versions
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
device_type,
currentOsVersion,
);
if (hupVersionInfo.versions.length === 0) {
throw new ExpectedError(
'There are no available Host OS update targets for this device',
);
}
return form.ask({
message: 'Target OS version',
type: 'list',
choices: hupVersionInfo.versions.map(version => ({
name:
hupVersionInfo.recommended === version
? `${version} (recommended)`
: version,
value: version,
})),
});
})
.then(version =>
patterns
.confirm(
options.yes || false,
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
)
.then(() => sdk.models.device.startOsUpdate(uuid, version))
.then(() => patterns.awaitDeviceOsUpdate(uuid, version)),
);
})
.nodeify(done);
// Get target OS version
let targetOsVersion = options.version;
if (targetOsVersion != null) {
if (!_.includes(hupVersionInfo.versions, targetOsVersion)) {
throw new ExpectedError(
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
);
}
} else {
targetOsVersion = await form.ask({
message: 'Target OS version',
type: 'list',
choices: hupVersionInfo.versions.map((version) => ({
name:
hupVersionInfo.recommended === version
? `${version} (recommended)`
: version,
value: version,
})),
});
}
// Confirm and start update
await patterns.confirm(
options.yes || false,
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
);
await sdk.models.device.startOsUpdate(uuid, targetOsVersion);
await patterns.awaitDeviceOsUpdate(uuid, targetOsVersion);
},
};

View File

@ -1,150 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
_ = require('lodash')
capitano = require('capitano')
columnify = require('columnify')
messages = require('../utils/messages')
{ getManualSortCompareFunction } = require('../utils/helpers')
{ exitWithExpectedError } = require('../utils/patterns')
{ getOclifHelpLinePairs } = require('./help_ts')
parse = (object) ->
return _.map object, (item) ->
# Hacky way to determine if an object is
# a function or a command
if item.alias?
signature = item.toString()
else
signature = item.signature.toString()
return [
signature
item.description
]
indent = (text) ->
text = _.map text.split('\n'), (line) ->
return ' ' + line
return text.join('\n')
print = (usageDescriptionPairs) ->
console.log indent columnify _.fromPairs(usageDescriptionPairs),
showHeaders: false
minWidth: 35
manuallySortedPrimaryCommands = [
'help',
'login',
'push',
'logs',
'ssh',
'apps',
'app',
'devices',
'device',
'tunnel',
'preload',
'build',
'deploy',
'join',
'leave',
'local scan',
]
general = (params, options, done) ->
console.log('Usage: balena [COMMAND] [OPTIONS]\n')
console.log(messages.reachingOut)
console.log('\nPrimary commands:\n')
# We do not want the wildcard command
# to be printed in the help screen.
commands = _.reject capitano.state.commands, (command) ->
return command.hidden or command.isWildcard()
groupedCommands = _.groupBy commands, (command) ->
if command.primary
return 'primary'
return 'secondary'
print parse(groupedCommands.primary).sort(getManualSortCompareFunction(
manuallySortedPrimaryCommands,
([signature, description], manualItem) ->
signature == manualItem or signature.startsWith("#{manualItem} ")
))
if options.verbose
console.log('\nAdditional commands:\n')
secondaryCommandPromise = getOclifHelpLinePairs()
.then (oclifHelpLinePairs) ->
print parse(groupedCommands.secondary).concat(oclifHelpLinePairs).sort()
else
console.log('\nRun `balena help --verbose` to list additional commands')
secondaryCommandPromise = Promise.resolve()
secondaryCommandPromise
.then ->
if not _.isEmpty(capitano.state.globalOptions)
console.log('\nGlobal Options:\n')
print parse(capitano.state.globalOptions).sort()
done()
.catch(done)
command = (params, options, done) ->
capitano.state.getMatchCommand params.command, (error, command) ->
return done(error) if error?
if not command? or command.isWildcard()
exitWithExpectedError("Command not found: #{params.command}")
console.log("Usage: #{command.signature}")
if command.help?
console.log("\n#{command.help}")
else if command.description?
console.log("\n#{_.capitalize(command.description)}")
if not _.isEmpty(command.options)
console.log('\nOptions:\n')
print parse(command.options).sort()
return done()
exports.help =
signature: 'help [command...]'
description: 'show help'
help: '''
Get detailed help for an specific command.
Examples:
$ balena help apps
$ balena help os download
'''
primary: true
options: [
signature: 'verbose'
description: 'show additional commands'
boolean: true
alias: 'v'
]
action: (params, options, done) ->
if params.command?
command(params, options, done)
else
general(params, options, done)

190
lib/actions/help.js Normal file
View File

@ -0,0 +1,190 @@
/*
Copyright 2016-2020 Balena Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as _ from 'lodash';
import * as capitano from 'capitano';
import * as columnify from 'columnify';
import * as messages from '../utils/messages';
import { getManualSortCompareFunction } from '../utils/helpers';
import { exitWithExpectedError } from '../errors';
import { getOclifHelpLinePairs } from './help_ts';
const parse = (object) =>
_.map(object, function (item) {
// Hacky way to determine if an object is
// a function or a command
let signature;
if (item.alias != null) {
signature = item.toString();
} else {
signature = item.signature.toString();
}
return [signature, item.description];
});
const indent = function (text) {
text = _.map(text.split('\n'), (line) => ' ' + line);
return text.join('\n');
};
const print = (usageDescriptionPairs) =>
console.log(
indent(
columnify(_.fromPairs(usageDescriptionPairs), {
showHeaders: false,
minWidth: 35,
}),
),
);
const manuallySortedPrimaryCommands = [
'help',
'login',
'push',
'logs',
'ssh',
'apps',
'app',
'devices',
'device',
'tunnel',
'preload',
'build',
'deploy',
'join',
'leave',
'local scan',
];
const general = function (_params, options, done) {
console.log('Usage: balena [COMMAND] [OPTIONS]\n');
console.log('Primary commands:\n');
// We do not want the wildcard command
// to be printed in the help screen.
const commands = capitano.state.commands.filter(
(command) => !command.hidden && !command.isWildcard(),
);
const capitanoCommands = _.groupBy(commands, function (command) {
if (command.primary) {
return 'primary';
}
return 'secondary';
});
return getOclifHelpLinePairs()
.then(function (oclifHelpLinePairs) {
const primaryHelpLinePairs = parse(capitanoCommands.primary)
.concat(oclifHelpLinePairs.primary)
.sort(
getManualSortCompareFunction(manuallySortedPrimaryCommands, function (
[signature],
manualItem,
) {
return (
signature === manualItem || signature.startsWith(`${manualItem} `)
);
}),
);
const secondaryHelpLinePairs = parse(capitanoCommands.secondary)
.concat(oclifHelpLinePairs.secondary)
.sort();
print(primaryHelpLinePairs);
if (options.verbose) {
console.log('\nAdditional commands:\n');
print(secondaryHelpLinePairs);
} else {
console.log(
'\nRun `balena help --verbose` to list additional commands',
);
}
if (!_.isEmpty(capitano.state.globalOptions)) {
console.log('\nGlobal Options:\n');
print(parse(capitano.state.globalOptions).sort());
}
console.log(indent('--debug\n'));
console.log(messages.help);
return done();
})
.catch(done);
};
const commandHelp = (params, _options, done) =>
capitano.state.getMatchCommand(params.command, function (error, command) {
if (error != null) {
return done(error);
}
if (command == null || command.isWildcard()) {
exitWithExpectedError(`Command not found: ${params.command}`);
}
console.log(`Usage: ${command.signature}`);
if (command.help != null) {
console.log(`\n${command.help}`);
} else if (command.description != null) {
console.log(`\n${_.capitalize(command.description)}`);
}
if (!_.isEmpty(command.options)) {
console.log('\nOptions:\n');
print(parse(command.options).sort());
}
console.log();
return done();
});
export const help = {
signature: 'help [command...]',
description: 'show help',
help: `\
Get detailed help for an specific command.
Examples:
$ balena help apps
$ balena help os download\
`,
primary: true,
options: [
{
signature: 'verbose',
description: 'show additional commands',
boolean: true,
alias: 'v',
},
],
action(params, options, done) {
if (params.command != null) {
return commandHelp(params, options, done);
} else {
return general(params, options, done);
}
},
};

View File

@ -15,32 +15,40 @@
* limitations under the License.
*/
import { Command } from '@oclif/command';
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import * as path from 'path';
import Command from '../command';
import { capitanoizeOclifUsage } from '../utils/oclif-utils';
export async function getOclifHelpLinePairs(): Promise<
Array<[string, string]>
> {
export async function getOclifHelpLinePairs() {
const { convertedCommands } = await import('../preparser');
const cmdClasses: Array<Promise<typeof Command>> = [];
const primary: Array<[string, string]> = [];
const secondary: Array<[string, string]> = [];
for (const convertedCmd of convertedCommands) {
const [topic, cmd] = convertedCmd.split(':');
const pathComponents = ['..', 'actions-oclif', topic];
if (cmd) {
pathComponents.push(cmd);
}
// note that `import(path)` returns a promise
cmdClasses.push(import(path.join(...pathComponents)));
const cmdModule = await import(path.join(...pathComponents));
const command: typeof Command = cmdModule.default;
if (!command.hidden) {
if (command.primary) {
primary.push(getCmdUsageDescriptionLinePair(command));
} else {
secondary.push(getCmdUsageDescriptionLinePair(command));
}
}
}
return Bluebird.map(cmdClasses, getCmdUsageDescriptionLinePair);
return { primary, secondary };
}
function getCmdUsageDescriptionLinePair(cmdModule: any): [string, string] {
const cmd: typeof Command = cmdModule.default;
function getCmdUsageDescriptionLinePair(cmd: typeof Command): [string, string] {
const usage = capitanoizeOclifUsage(cmd.usage);
let description = '';
// note: [^] matches any characters (including line breaks), achieving the

View File

@ -1,41 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
module.exports =
apiKey: require('./api-key')
app: require('./app')
auth: require('./auth')
device: require('./device')
tags: require('./tags')
keys: require('./keys')
logs: require('./logs')
local: require('./local')
scan: require('./scan')
notes: require('./notes')
help: require('./help')
os: require('./os')
settings: require('./settings')
config: require('./config')
ssh: require('./ssh')
internal: require('./internal')
build: require('./build')
deploy: require('./deploy')
util: require('./util')
preload: require('./preload')
push: require('./push')
join: require('./join')
leave: require('./leave')
tunnel: require('./tunnel')

35
lib/actions/index.ts Normal file
View File

@ -0,0 +1,35 @@
/*
Copyright 2016-2020 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as auth from './auth';
import * as config from './config';
import * as device from './device';
import * as help from './help';
import * as local from './local';
import * as logs from './logs';
import * as os from './os';
import * as push from './push';
import * as ssh from './ssh';
import * as tunnel from './tunnel';
import * as util from './util';
export { auth, device, logs, local, help, os, config, ssh, util, push, tunnel };
export { build } from './build';
export { deploy } from './deploy';
export { preload } from './preload';

View File

@ -1,56 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
# These are internal commands we want to be runnable from the outside
# One use-case for this is spawning the minimal operation with root priviledges
exports.osInit =
signature: 'internal osinit <image> <type> <config>'
description: 'do actual init of the device with the preconfigured os image'
help: '''
Don't use this command directly! Use `balena os initialize <image>` instead.
'''
hidden: true
root: true
action: (params, options, done) ->
Promise = require('bluebird')
init = require('balena-device-init')
helpers = require('../utils/helpers')
configPromise = Promise.try -> JSON.parse(params.config)
manifestPromise = helpers.getManifest(params.image, params.type)
Promise.join configPromise, manifestPromise, (config, manifest) ->
init.initialize(params.image, manifest, config)
.then(helpers.osProgressHandler)
.nodeify(done)
exports.scanDevices =
signature: 'internal scandevices'
description: 'scan for local balena-enabled devices and show a picker to choose one'
help: '''
Don't use this command directly!
'''
hidden: true
root: true
action: (params, options, done) ->
Promise = require('bluebird')
{ forms } = require('balena-sync')
return Promise.try ->
forms.selectLocalBalenaOsDevice()
.then (hostnameOrIp) ->
console.error("==> Selected device: #{hostnameOrIp}")
.nodeify(done)

View File

@ -1,74 +0,0 @@
/*
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Bluebird from 'bluebird';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
interface Args {
deviceIp?: string;
}
interface Options {
application?: string;
}
export const join: CommandDefinition<Args, Options> = {
signature: 'join [deviceIp]',
description:
'Promote a local device running balenaOS to join an application on a balena server',
help: stripIndent`
Use this command to move a local device to an application on another balena server.
For example, you could provision a device against an openBalena installation
where you perform end-to-end tests and then move it to balenaCloud when it's
ready for production.
Moving a device between applications on the same server is not supported.
If you don't specify a device hostname or IP, this command will automatically
scan the local network for balenaOS devices and prompt you to select one
from an interactive picker. This usually requires root privileges.
Examples:
$ balena join
$ balena join balena.local
$ balena join balena.local --application MyApp
$ balena join 192.168.1.25
$ balena join 192.168.1.25 --application MyApp
`,
options: [
{
signature: 'application',
parameter: 'application',
alias: 'a',
description: 'The name of the application the device should join',
},
],
primary: true,
async action(params, options, done) {
const balena = await import('balena-sdk');
const Logger = await import('../utils/logger');
const promote = await import('../utils/promote');
const sdk = balena.fromSharedOptions();
const logger = Logger.getLogger();
return Bluebird.try(() => {
return promote.join(logger, sdk, params.deviceIp, options.application);
}).nodeify(done);
},
};

View File

@ -1,123 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
commandOptions = require('./command-options')
exports.list =
signature: 'keys'
description: 'list all ssh keys'
help: '''
Use this command to list all your SSH keys.
Examples:
$ balena keys
'''
permission: 'user'
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
balena.models.key.getAll().then (keys) ->
console.log visuals.table.horizontal keys, [
'id'
'title'
]
.nodeify(done)
exports.info =
signature: 'key <id>'
description: 'list a single ssh key'
help: '''
Use this command to show information about a single SSH key.
Examples:
$ balena key 17
'''
permission: 'user'
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
balena.models.key.get(params.id).then (key) ->
console.log visuals.table.vertical key, [
'id'
'title'
]
# Since the public key string is long, it might
# wrap to lines below, causing the table layout to break.
# See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key)
.nodeify(done)
exports.remove =
signature: 'key rm <id>'
description: 'remove a ssh key'
help: '''
Use this command to remove a SSH key from balena.
Notice this command asks for confirmation interactively.
You can avoid this by passing the `--yes` boolean option.
Examples:
$ balena key rm 17
$ balena key rm 17 --yes
'''
options: [ commandOptions.yes ]
permission: 'user'
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
patterns.confirm(options.yes, 'Are you sure you want to delete the key?').then ->
balena.models.key.remove(params.id)
.nodeify(done)
exports.add =
signature: 'key add <name> [path]'
description: 'add a SSH key to balena'
help: '''
Use this command to associate a new SSH key with your account.
If `path` is omitted, the command will attempt
to read the SSH key from stdin.
Examples:
$ balena key add Main ~/.ssh/id_rsa.pub
$ cat ~/.ssh/id_rsa.pub | balena key add Main
'''
permission: 'user'
action: (params, options, done) ->
_ = require('lodash')
Promise = require('bluebird')
readFileAsync = Promise.promisify(require('fs').readFile)
capitano = require('capitano')
balena = require('balena-sdk').fromSharedOptions()
Promise.try ->
return readFileAsync(params.path, encoding: 'utf8') if params.path?
# TODO: should this be promisified for consistency?
Promise.fromNode (callback) ->
capitano.utils.getStdin (data) ->
return callback(null, data)
.then(_.partial(balena.models.key.create, params.name))
.nodeify(done)

View File

@ -1,59 +0,0 @@
/*
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Bluebird from 'bluebird';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
interface Args {
deviceIp?: string;
}
export const leave: CommandDefinition<Args, {}> = {
signature: 'leave [deviceIp]',
description: 'Detach a local device from its balena application',
help: stripIndent`
Use this command to make a local device leave the balena server it is
provisioned on. This effectively makes the device "unmanaged".
The device entry on the server is preserved after running this command,
so the device can subsequently re-join the server if needed.
If you don't specify a device hostname or IP, this command will automatically
scan the local network for balenaOS devices and prompt you to select one
from an interactive picker. This usually requires root privileges.
Examples:
$ balena leave
$ balena leave balena.local
$ balena leave 192.168.1.25
`,
options: [],
permission: 'user',
primary: true,
async action(params, _options, done) {
const balena = await import('balena-sdk');
const Logger = await import('../utils/logger');
const promote = await import('../utils/promote');
const sdk = balena.fromSharedOptions();
const logger = Logger.getLogger();
return Bluebird.try(() => {
return promote.leave(logger, sdk, params.deviceIp);
}).nodeify(done);
},
};

View File

@ -1,62 +0,0 @@
Promise = require('bluebird')
_ = require('lodash')
chalk = require('chalk')
dockerUtils = require('../../utils/docker')
{ exitWithExpectedError } = require('../../utils/patterns')
exports.dockerPort = dockerPort = 2375
exports.dockerTimeout = dockerTimeout = 2000
exports.filterOutSupervisorContainer = filterOutSupervisorContainer = (container) ->
for name in container.Names
return false if (name.includes('resin_supervisor') or name.includes('balena_supervisor'))
return true
exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor = false) ->
form = require('resin-cli-form')
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
# List all containers, including those not running
docker.listContainersAsync(all: true)
.filter (container) ->
return true if not filterSupervisor
filterOutSupervisorContainer(container)
.then (containers) ->
if _.isEmpty(containers)
exitWithExpectedError("No containers found in #{deviceIp}")
return form.ask
message: 'Select a container'
type: 'list'
choices: _.map containers, (container) ->
containerName = container.Names?[0] or 'Untitled'
shortContainerId = ('' + container.Id).substr(0, 11)
return {
name: "#{containerName} (#{shortContainerId})"
value: container.Id
}
exports.pipeContainerStream = Promise.method ({ deviceIp, name, outStream, follow = false }) ->
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort)
container = docker.getContainer(name)
container.inspectAsync()
.then (containerInfo) ->
return containerInfo?.State?.Running
.then (isRunning) ->
container.attachAsync
logs: not follow or not isRunning
stream: follow and isRunning
stdout: true
stderr: true
.then (containerStream) ->
containerStream.pipe(outStream)
.catch (err) ->
err = '' + err.statusCode
if err is '404'
return console.log(chalk.red.bold("Container '#{name}' not found."))
throw err
exports.getSubShellCommand = require('../../utils/helpers').getSubShellCommand

View File

@ -0,0 +1,97 @@
import * as Promise from 'bluebird';
import * as _ from 'lodash';
import * as dockerUtils from '../../utils/docker';
import { exitWithExpectedError } from '../../errors';
import { getChalk } from '../../utils/lazy';
export const dockerPort = 2375;
export const dockerTimeout = 2000;
export const filterOutSupervisorContainer = function (container) {
for (const name of container.Names) {
if (
name.includes('resin_supervisor') ||
name.includes('balena_supervisor')
) {
return false;
}
}
return true;
};
export const selectContainerFromDevice = Promise.method(function (
deviceIp,
filterSupervisor,
) {
if (filterSupervisor == null) {
filterSupervisor = false;
}
const form = require('resin-cli-form');
const docker = dockerUtils.createClient({
host: deviceIp,
port: dockerPort,
timeout: dockerTimeout,
});
// List all containers, including those not running
return docker.listContainers({ all: true }).then(function (containers) {
containers = containers.filter(function (container) {
if (!filterSupervisor) {
return true;
}
return filterOutSupervisorContainer(container);
});
if (_.isEmpty(containers)) {
exitWithExpectedError(`No containers found in ${deviceIp}`);
}
return form.ask({
message: 'Select a container',
type: 'list',
choices: _.map(containers, function (container) {
const containerName = container.Names?.[0] || 'Untitled';
const shortContainerId = ('' + container.Id).substr(0, 11);
return {
name: `${containerName} (${shortContainerId})`,
value: container.Id,
};
}),
});
});
});
export const pipeContainerStream = Promise.method(function ({
deviceIp,
name,
outStream,
follow,
}) {
if (follow == null) {
follow = false;
}
const docker = dockerUtils.createClient({ host: deviceIp, port: dockerPort });
const container = docker.getContainer(name);
return container
.inspect()
.then((containerInfo) => containerInfo?.State?.Running)
.then((isRunning) =>
container.attach({
logs: !follow || !isRunning,
stream: follow && isRunning,
stdout: true,
stderr: true,
}),
)
.then((containerStream) => containerStream.pipe(outStream))
.catch(function (err) {
err = '' + err.statusCode;
if (err === '404') {
return console.log(
getChalk().red.bold(`Container '${name}' not found.`),
);
}
throw err;
});
});

View File

@ -1,237 +0,0 @@
###
Copyright 2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
BOOT_PARTITION = 1
CONNECTIONS_FOLDER = '/system-connections'
getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
mapper: [
{
template:
persistentLogging: '{{persistentLogging}}'
domain: [
[ 'config_json', 'persistentLogging' ]
]
}
{
template:
hostname: '{{hostname}}'
domain: [
[ 'config_json', 'hostname' ]
]
}
{
template:
wifi:
ssid: '{{networkSsid}}'
'wifi-security':
psk: '{{networkKey}}'
domain: [
[ 'system_connections', connnectionFileName, 'wifi' ]
[ 'system_connections', connnectionFileName, 'wifi-security' ]
]
}
]
files:
system_connections:
fileset: true
type: 'ini'
location:
path: CONNECTIONS_FOLDER.slice(1)
# Reconfix still uses the older resin-image-fs, so still needs an
# object-based partition definition.
partition: BOOT_PARTITION
config_json:
type: 'json'
location:
path: 'config.json'
partition: BOOT_PARTITION
inquirerOptions = (data) -> [
{
message: 'Network SSID'
type: 'input'
name: 'networkSsid'
default: data.networkSsid
}
{
message: 'Network Key'
type: 'input'
name: 'networkKey'
default: data.networkKey
}
{
message: 'Do you want to set advanced settings?'
type: 'confirm'
name: 'advancedSettings'
default: false
}
{
message: 'Device Hostname'
type: 'input'
name: 'hostname'
default: data.hostname,
when: (answers) ->
answers.advancedSettings
}
{
message: 'Do you want to enable persistent logging?'
type: 'confirm'
name: 'persistentLogging'
default: data.persistentLogging
when: (answers) ->
answers.advancedSettings
}
]
getConfiguration = (data) ->
_ = require('lodash')
inquirer = require('inquirer')
# `persistentLogging` can be `undefined`, so we want
# to make sure that case defaults to `false`
data = _.assign data,
persistentLogging: data.persistentLogging or false
inquirer.prompt(inquirerOptions(data))
.then (answers) ->
return _.merge(data, answers)
# Taken from https://goo.gl/kr1kCt
CONNECTION_FILE = '''
[connection]
id=resin-wifi
type=wifi
[wifi]
hidden=true
mode=infrastructure
ssid=My_Wifi_Ssid
[wifi-security]
auth-alg=open
key-mgmt=wpa-psk
psk=super_secret_wifi_password
[ipv4]
method=auto
[ipv6]
addr-gen-mode=stable-privacy
method=auto
'''
###
* if the `resin-wifi` file exists (previously configured image or downloaded from the UI) it's used and reconfigured
* if the `resin-sample.ignore` exists it's copied to `resin-wifi`
* if the `resin-sample` exists it's reconfigured (legacy mode, will be removed eventually)
* otherwise, the new file is created
###
prepareConnectionFile = (target) ->
_ = require('lodash')
imagefs = require('resin-image-fs')
imagefs.listDirectory
image: target
partition: BOOT_PARTITION
path: CONNECTIONS_FOLDER
.then (files) ->
# The required file already exists
if _.includes(files, 'resin-wifi')
return null
# Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
if _.includes(files, 'resin-sample.ignore')
return imagefs.copy
image: target
partition: BOOT_PARTITION
path: "#{CONNECTIONS_FOLDER}/resin-sample.ignore"
,
image: target
partition: BOOT_PARTITION
path: "#{CONNECTIONS_FOLDER}/resin-wifi"
.thenReturn(null)
# Legacy mode, to be removed later
# We return the file name override from this branch
# When it is removed the following cleanup should be done:
# * delete all the null returns from this method
# * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi`
# * drop the final `then` from this method
# * adapt the code in the main listener to not receive the config from this method, and use that constant instead
if _.includes(files, 'resin-sample')
return 'resin-sample'
# In case there's no file at all (shouldn't happen normally, but the file might have been removed)
return imagefs.writeFile
image: target
partition: BOOT_PARTITION
path: "#{CONNECTIONS_FOLDER}/resin-wifi"
, CONNECTION_FILE
.thenReturn(null)
.then (connectionFileName) ->
return getConfigurationSchema(connectionFileName)
removeHostname = (schema) ->
_ = require('lodash')
schema.mapper = _.reject schema.mapper, (mapper) ->
_.isEqual(Object.keys(mapper.template), ['hostname'])
module.exports =
signature: 'local configure <target>'
description: '(Re)configure a balenaOS drive or image'
help: '''
Use this command to configure or reconfigure a balenaOS drive or image.
Examples:
$ balena local configure /dev/sdc
$ balena local configure path/to/image.img
'''
root: true
action: (params, options, done) ->
Promise = require('bluebird')
path = require('path')
umount = require('umount')
umountAsync = Promise.promisify(umount.umount)
isMountedAsync = Promise.promisify(umount.isMounted)
reconfix = require('reconfix')
denymount = Promise.promisify(require('denymount'))
prepareConnectionFile(params.target)
.tap ->
isMountedAsync(params.target).then (isMounted) ->
return if not isMounted
umountAsync(params.target)
.then (configurationSchema) ->
dmOpts = {}
if process.pkg
# when running in a standalone pkg install, the 'denymount'
# executable is placed on the same folder as process.execPath
dmOpts.executablePath = path.join(path.dirname(process.execPath), 'denymount')
dmHandler = (cb) ->
reconfix.readConfiguration(configurationSchema, params.target)
.then(getConfiguration)
.then (answers) ->
if not answers.hostname
removeHostname(configurationSchema)
reconfix.writeConfiguration(configurationSchema, answers, params.target)
.asCallback(cb)
denymount params.target, dmHandler, dmOpts
.then ->
console.log('Done!')
.asCallback(done)

View File

@ -0,0 +1,285 @@
/*
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.
*/
const BOOT_PARTITION = 1;
const CONNECTIONS_FOLDER = '/system-connections';
const getConfigurationSchema = function (connnectionFileName) {
if (connnectionFileName == null) {
connnectionFileName = 'resin-wifi';
}
return {
mapper: [
{
template: {
persistentLogging: '{{persistentLogging}}',
},
domain: [['config_json', 'persistentLogging']],
},
{
template: {
hostname: '{{hostname}}',
},
domain: [['config_json', 'hostname']],
},
{
template: {
wifi: {
ssid: '{{networkSsid}}',
},
'wifi-security': {
psk: '{{networkKey}}',
},
},
domain: [
['system_connections', connnectionFileName, 'wifi'],
['system_connections', connnectionFileName, 'wifi-security'],
],
},
],
files: {
system_connections: {
fileset: true,
type: 'ini',
location: {
path: CONNECTIONS_FOLDER.slice(1),
// Reconfix still uses the older resin-image-fs, so still needs an
// object-based partition definition.
partition: BOOT_PARTITION,
},
},
config_json: {
type: 'json',
location: {
path: 'config.json',
partition: BOOT_PARTITION,
},
},
},
};
};
const inquirerOptions = (data) => [
{
message: 'Network SSID',
type: 'input',
name: 'networkSsid',
default: data.networkSsid,
},
{
message: 'Network Key',
type: 'input',
name: 'networkKey',
default: data.networkKey,
},
{
message: 'Do you want to set advanced settings?',
type: 'confirm',
name: 'advancedSettings',
default: false,
},
{
message: 'Device Hostname',
type: 'input',
name: 'hostname',
default: data.hostname,
when(answers) {
return answers.advancedSettings;
},
},
{
message: 'Do you want to enable persistent logging?',
type: 'confirm',
name: 'persistentLogging',
default: data.persistentLogging,
when(answers) {
return answers.advancedSettings;
},
},
];
const getConfiguration = function (data) {
const _ = require('lodash');
const inquirer = require('inquirer');
// `persistentLogging` can be `undefined`, so we want
// to make sure that case defaults to `false`
data = _.assign(data, { persistentLogging: data.persistentLogging || false });
return inquirer
.prompt(inquirerOptions(data))
.then((answers) => _.merge(data, answers));
};
// Taken from https://goo.gl/kr1kCt
const CONNECTION_FILE = `\
[connection]
id=resin-wifi
type=wifi
[wifi]
hidden=true
mode=infrastructure
ssid=My_Wifi_Ssid
[wifi-security]
auth-alg=open
key-mgmt=wpa-psk
psk=super_secret_wifi_password
[ipv4]
method=auto
[ipv6]
addr-gen-mode=stable-privacy
method=auto\
`;
/*
* if the `resin-wifi` file exists (previously configured image or downloaded from the UI) it's used and reconfigured
* if the `resin-sample.ignore` exists it's copied to `resin-wifi`
* if the `resin-sample` exists it's reconfigured (legacy mode, will be removed eventually)
* otherwise, the new file is created
*/
const prepareConnectionFile = function (target) {
const _ = require('lodash');
const imagefs = require('resin-image-fs');
return imagefs
.listDirectory({
image: target,
partition: BOOT_PARTITION,
path: CONNECTIONS_FOLDER,
})
.then(function (files) {
// The required file already exists
if (_.includes(files, 'resin-wifi')) {
return null;
}
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
if (_.includes(files, 'resin-sample.ignore')) {
return imagefs
.copy(
{
image: target,
partition: BOOT_PARTITION,
path: `${CONNECTIONS_FOLDER}/resin-sample.ignore`,
},
{
image: target,
partition: BOOT_PARTITION,
path: `${CONNECTIONS_FOLDER}/resin-wifi`,
},
)
.thenReturn(null);
}
// Legacy mode, to be removed later
// We return the file name override from this branch
// When it is removed the following cleanup should be done:
// * delete all the null returns from this method
// * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi`
// * drop the final `then` from this method
// * adapt the code in the main listener to not receive the config from this method, and use that constant instead
if (_.includes(files, 'resin-sample')) {
return 'resin-sample';
}
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
return imagefs
.writeFile(
{
image: target,
partition: BOOT_PARTITION,
path: `${CONNECTIONS_FOLDER}/resin-wifi`,
},
CONNECTION_FILE,
)
.thenReturn(null);
})
.then((connectionFileName) => getConfigurationSchema(connectionFileName));
};
const removeHostname = function (schema) {
const _ = require('lodash');
schema.mapper = _.reject(schema.mapper, (mapper) =>
_.isEqual(Object.keys(mapper.template), ['hostname']),
);
};
export const configure = {
signature: 'local configure <target>',
description: '(Re)configure a balenaOS drive or image',
help: `\
Use this command to configure or reconfigure a balenaOS drive or image.
Examples:
$ balena local configure /dev/sdc
$ balena local configure path/to/image.img\
`,
root: true,
action(params) {
const Promise = require('bluebird');
const path = require('path');
const umount = require('umount');
const umountAsync = Promise.promisify(umount.umount);
const isMountedAsync = Promise.promisify(umount.isMounted);
const reconfix = require('reconfix');
const denymount = Promise.promisify(require('denymount'));
return prepareConnectionFile(params.target)
.tap(() =>
isMountedAsync(params.target).then(function (isMounted) {
if (!isMounted) {
return;
}
return umountAsync(params.target);
}),
)
.then(function (configurationSchema) {
const dmOpts = {};
if (process.pkg) {
// when running in a standalone pkg install, the 'denymount'
// executable is placed on the same folder as process.execPath
dmOpts.executablePath = path.join(
path.dirname(process.execPath),
'denymount',
);
}
const dmHandler = (cb) =>
reconfix
.readConfiguration(configurationSchema, params.target)
.then(getConfiguration)
.then(function (answers) {
if (!answers.hostname) {
removeHostname(configurationSchema);
}
return reconfix.writeConfiguration(
configurationSchema,
answers,
params.target,
);
})
.asCallback(cb);
return denymount(params.target, dmHandler, dmOpts);
})
.then(() => {
console.log('Done!');
});
},
};

View File

@ -15,32 +15,29 @@ limitations under the License.
*/
import { CommandDefinition } from 'capitano';
import chalk from 'chalk';
import { stripIndent } from 'common-tags';
import * as SDK from 'etcher-sdk';
import { getChalk, getVisuals } from '../../utils/lazy';
async function getDrive(options: {
drive?: string;
}): Promise<SDK.sourceDestination.BlockDevice> {
const drive = options.drive || (await getVisuals().drive('Select a drive'));
const sdk = await import('etcher-sdk');
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
const scanner = new sdk.scanner.Scanner([adapter]);
await scanner.start();
let drive: SDK.sourceDestination.BlockDevice;
if (options.drive !== undefined) {
const d = scanner.getBy('device', options.drive);
try {
const d = scanner.getBy('device', drive);
if (d === undefined || !(d instanceof sdk.sourceDestination.BlockDevice)) {
throw new Error(`Drive not found: ${options.drive}`);
}
drive = d;
} else {
const { DriveList } = await import('../../utils/visuals/drive-list');
const driveList = new DriveList(scanner);
drive = await driveList.run();
return d;
} finally {
scanner.stop();
}
scanner.stop();
return drive;
}
export const flash: CommandDefinition<
@ -73,7 +70,6 @@ export const flash: CommandDefinition<
},
],
async action(params, options) {
const visuals = await import('resin-cli-visuals');
const form = await import('resin-cli-form');
const { sourceDestination, multiWrite } = await import('etcher-sdk');
@ -88,7 +84,7 @@ export const flash: CommandDefinition<
default: false,
}));
if (yes !== true) {
console.log(chalk.red.bold('Aborted image flash'));
console.log(getChalk().red.bold('Aborted image flash'));
process.exit(0);
}
@ -98,6 +94,7 @@ export const flash: CommandDefinition<
);
const source = await file.getInnerSource();
const visuals = getVisuals();
const progressBars: { [key: string]: any } = {
flashing: new visuals.Progress('Flashing'),
verifying: new visuals.Progress('Validating'),
@ -108,7 +105,7 @@ export const flash: CommandDefinition<
[drive],
(_, error) => {
// onFail
console.log(chalk.red.bold(error.message));
console.log(getChalk().red.bold(error.message));
},
(progress: SDK.multiWrite.MultiDestinationProgress) => {
// onProgress

View File

@ -1,5 +1,5 @@
###
Copyright 2017 Balena
/*
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.
@ -12,7 +12,7 @@ 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.
###
*/
exports.configure = require('./configure')
exports.flash = require('./flash').flash
export { configure } from './configure';
export { flash } from './flash';

View File

@ -1,66 +0,0 @@
###
Copyright 2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
# A function to reliably execute a command
# in all supported operating systems, including
# different Windows environments like `cmd.exe`
# and `Cygwin` should be encapsulated in a
# re-usable package.
#
module.exports =
signature: 'local logs [deviceIp]'
description: 'Get or attach to logs of a running container on a balenaOS device'
help: '''
Examples:
$ balena local logs
$ balena local logs -f
$ balena local logs 192.168.1.10
$ balena local logs 192.168.1.10 -f
$ balena local logs 192.168.1.10 -f --app-name myapp
'''
options: [
signature: 'follow'
boolean: true
description: 'follow log'
alias: 'f'
,
signature: 'app-name'
parameter: 'name'
description: 'name of container to get logs from'
alias: 'a'
]
root: true
action: (params, options, done) ->
Promise = require('bluebird')
{ forms } = require('balena-sync')
{ selectContainerFromDevice, pipeContainerStream } = require('./common')
Promise.try ->
if not params.deviceIp?
return forms.selectLocalBalenaOsDevice()
return params.deviceIp
.then (@deviceIp) =>
if not options['app-name']?
return selectContainerFromDevice(@deviceIp)
return options['app-name']
.then (appName) =>
pipeContainerStream
deviceIp: @deviceIp
name: appName
outStream: process.stdout
follow: options['follow']

View File

@ -1,95 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
# Loads '.balena-sync.yml' configuration from 'source' directory.
# Returns the configuration object on success
#
_ = require('lodash')
balenaPush = require('balena-sync').capitano('balena-toolbox')
originalAction = balenaPush.action
# TODO: This is a temporary workaround to reuse the existing `rdt push`
# capitano frontend in `balena local push`.
# coffeelint: disable-next-line ("Line ends with trailing whitespace")
deprecationMsg = '''
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Deprecation notice: `balena local push` is deprecated and will be removed in a
future release of the CLI. Please use `balena push <ipAddress>` instead.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'''
balenaPushHelp = """#{deprecationMsg}
Use this command to push your local changes to a container on a LAN-accessible
balenaOS device on the fly.
This command requires an openssh-compatible 'ssh' client and 'rsync' to be
available in the executable PATH of the shell environment. For more information
(including Windows support) please check the README at:
https://github.com/balena-io/balena-cli
If `Dockerfile` or any file in the 'build-triggers' list is changed,
a new container will be built and run on your device.
If not, changes will simply be synced with `rsync` into the application container.
After every 'balena local push' the updated settings will be saved in
'<source>/.balena-sync.yml' and will be used in later invocations. You can
also change any option by editing '.balena-sync.yml' directly.
Here is an example '.balena-sync.yml' :
$ cat $PWD/.balena-sync.yml
local_balenaos:
app-name: local-app
build-triggers:
- Dockerfile: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
- package.json: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
environment:
- MY_VARIABLE=123
Command line options have precedence over the ones saved in '.balena-sync.yml'.
If '.gitignore' is found in the source directory then all explicitly listed files will be
excluded when using rsync to update the container. You can choose to change this default behavior with the
'--skip-gitignore' option.
Examples:
$ balena local push
$ balena local push --app-name test-server --build-triggers package.json,requirements.txt
$ balena local push --force-build
$ balena local push --force-build --skip-logs
$ balena local push --ignore lib/
$ balena local push --verbose false
$ balena local push 192.168.2.10 --source . --destination /usr/src/app
$ balena local push 192.168.2.10 -s /home/user/balenaProject -d /usr/src/app --before 'echo Hello' --after 'echo Done'
"""
module.exports = _.assign balenaPush,
signature: 'local push [deviceIp]'
description: '[deprecated: use "balena push ipAddress"] ' + balenaPush.description
help: balenaPushHelp
primary: false
root: true
action: (params, options, done) ->
console.log deprecationMsg
originalAction(params, options, done)

View File

@ -1,114 +0,0 @@
###
Copyright 2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
{ hostOSAccess } = require('../command-options')
_ = require('lodash')
localHostOSAccessOption = _.cloneDeep(hostOSAccess)
localHostOSAccessOption.description = 'get a shell into the host OS'
module.exports =
signature: 'local ssh [deviceIp]'
description: 'Get a shell into a balenaOS device'
help: '''
Warning: 'balena local ssh' requires an openssh-compatible client to be correctly
installed in your shell environment. For more information (including Windows
support) please check the README here: https://github.com/balena-io/balena-cli
Use this command to get a shell into the running application container of
your device.
The '--host' option will get you a shell into the Host OS of the balenaOS device.
No option will return a list of containers to enter or you can explicitly select
one by passing its name to the --container option
Examples:
$ balena local ssh
$ balena local ssh --host
$ balena local ssh --container chaotic_water
$ balena local ssh --container chaotic_water --port 22222
$ balena local ssh --verbose
'''
options: [
signature: 'verbose'
boolean: true
description: 'increase verbosity'
alias: 'v'
,
localHostOSAccessOption,
signature: 'container'
parameter: 'container'
default: null
description: 'name of container to access'
alias: 'c'
,
signature: 'port'
parameter: 'port'
description: 'ssh port number (default: 22222)'
alias: 'p'
]
root: true
action: (params, options, done) ->
child_process = require('child_process')
Promise = require 'bluebird'
_ = require('lodash')
{ forms } = require('balena-sync')
{ selectContainerFromDevice, getSubShellCommand } = require('./common')
{ exitWithExpectedError } = require('../../utils/patterns')
if (options.host is true and options.container?)
exitWithExpectedError('Please pass either --host or --container option')
if not options.port?
options.port = 22222
verbose = if options.verbose then '-vvv' else ''
Promise.try ->
if not params.deviceIp?
return forms.selectLocalBalenaOsDevice()
return params.deviceIp
.then (deviceIp) ->
_.assign(options, { deviceIp })
return if options.host
if not options.container?
return selectContainerFromDevice(deviceIp)
return options.container
.then (container) ->
command = "ssh \
#{verbose} \
-t \
-p #{options.port} \
-o LogLevel=ERROR \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
root@#{options.deviceIp}"
if not options.host
shellCmd = '''/bin/sh -c $"'if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi'"'''
dockerCmd = "'$(if [ -f /usr/bin/balena ]; then echo \"balena\"; else echo \"docker\"; fi)'"
command += " #{dockerCmd} exec -ti #{container} #{shellCmd}"
subShellCommand = getSubShellCommand(command)
child_process.spawn subShellCommand.program, subShellCommand.args,
stdio: 'inherit'
.nodeify(done)

View File

@ -1,79 +0,0 @@
###
Copyright 2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
# A function to reliably execute a command
# in all supported operating systems, including
# different Windows environments like `cmd.exe`
# and `Cygwin` should be encapsulated in a
# re-usable package.
#
module.exports =
signature: 'local stop [deviceIp]'
description: 'Stop a running container on a balenaOS device'
help: '''
Examples:
$ balena local stop
$ balena local stop --app-name myapp
$ balena local stop --all
$ balena local stop 192.168.1.10
$ balena local stop 192.168.1.10 --app-name myapp
'''
options: [
signature: 'all'
boolean: true
description: 'stop all containers'
,
signature: 'app-name'
parameter: 'name'
description: 'name of container to stop'
alias: 'a'
]
root: true
action: (params, options, done) ->
Promise = require('bluebird')
chalk = require('chalk')
{ forms, config, BalenaLocalDockerUtils } = require('balena-sync')
{ selectContainerFromDevice, filterOutSupervisorContainer } = require('./common')
Promise.try ->
if not params.deviceIp?
return forms.selectLocalBalenaOsDevice()
return params.deviceIp
.then (@deviceIp) =>
@docker = new BalenaLocalDockerUtils(@deviceIp)
if options.all
# Only list running containers
return @docker.docker.listContainersAsync(all: false)
.filter(filterOutSupervisorContainer)
.then (containers) =>
Promise.map containers, ({ Names, Id }) =>
console.log(chalk.yellow.bold("* Stopping container #{Names[0]}"))
@docker.stopContainer(Id)
ymlConfig = config.load()
@appName = options['app-name'] ? ymlConfig['local_balenaos']?['app-name']
@docker.checkForRunningContainer(@appName)
.then (isRunning) =>
if not isRunning
return selectContainerFromDevice(@deviceIp, true)
console.log(chalk.yellow.bold("* Stopping container #{@appName}"))
return @appName
.then (runningContainerName) =>
@docker.stopContainer(runningContainerName)

View File

@ -14,25 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { LogMessage } from 'balena-sdk';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { getBalenaSdk } from '../utils/lazy';
import { normalizeUuidProp } from '../utils/normalization';
import { validateDotLocalUrl } from '../utils/validation';
type CloudLog =
| {
isSystem: false;
serviceId: number;
timestamp: number;
message: string;
}
| {
isSystem: true;
timestamp: number;
message: string;
};
export const logs: CommandDefinition<
{
uuidOrDevice: string;
@ -94,30 +83,28 @@ export const logs: CommandDefinition<
},
],
primary: true,
async action(params, options, done) {
async action(params, options) {
normalizeUuidProp(params);
const balena = (await import('balena-sdk')).fromSharedOptions();
const isArray = await import('lodash/isArray');
const balena = getBalenaSdk();
const { ExpectedError } = await import('../errors');
const { serviceIdToName } = await import('../utils/cloud');
const { displayDeviceLogs, displayLogObject } = await import(
'../utils/device/logs'
);
const { validateIPAddress } = await import('../utils/validation');
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
'../utils/patterns'
);
const { checkLoggedIn } = await import('../utils/patterns');
const Logger = await import('../utils/logger');
const logger = Logger.getLogger();
const servicesToDisplay =
options.service != null
? isArray(options.service)
? Array.isArray(options.service)
? options.service
: [options.service]
: undefined;
const displayCloudLog = async (line: CloudLog) => {
const displayCloudLog = async (line: LogMessage) => {
if (!line.isSystem) {
let serviceName = await serviceIdToName(balena, line.serviceId);
if (serviceName == null) {
@ -149,35 +136,33 @@ export const logs: CommandDefinition<
try {
await deviceApi.ping();
} catch (e) {
exitWithExpectedError(
new Error(
`Cannot access local mode device at address ${params.uuidOrDevice}`,
),
throw new ExpectedError(
`Cannot access local mode device at address ${params.uuidOrDevice}`,
);
}
const logStream = await deviceApi.getLogStream();
displayDeviceLogs(
await displayDeviceLogs(
logStream,
logger,
options.system || false,
servicesToDisplay,
);
} else {
await exitIfNotLoggedIn();
await checkLoggedIn();
if (options.tail) {
return balena.logs
.subscribe(params.uuidOrDevice, { count: 100 })
.then(function(logStream) {
logStream.on('line', displayCloudLog);
logStream.on('error', done);
})
.catch(done);
const logStream = await balena.logs.subscribe(params.uuidOrDevice, {
count: 100,
});
// Never resolve (quit with CTRL-C), but reject on a broken connection
await new Promise((_resolve, reject) => {
logStream.on('line', displayCloudLog);
logStream.on('error', reject);
});
} else {
return balena.logs
.history(params.uuidOrDevice)
.each(displayCloudLog)
.catch(done);
const logMessages = await balena.logs.history(params.uuidOrDevice);
for (const logMessage of logMessages) {
await displayCloudLog(logMessage);
}
}
}
},

View File

@ -1,55 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
{ normalizeUuidProp } = require('../utils/normalization')
exports.set =
signature: 'note <|note>'
description: 'set a device note'
help: '''
Use this command to set or update a device note.
If note command isn't passed, the tool attempts to read from `stdin`.
To view the notes, use $ balena device <uuid>.
Examples:
$ balena note "My useful note" --device 7cf02a6
$ cat note.txt | balena note --device 7cf02a6
'''
options: [
signature: 'device'
parameter: 'device'
description: 'device uuid'
alias: [ 'd', 'dev' ]
required: 'You have to specify a device'
]
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(options, 'device')
Promise = require('bluebird')
_ = require('lodash')
balena = require('balena-sdk').fromSharedOptions()
{ exitWithExpectedError } = require('../utils/patterns')
Promise.try ->
if _.isEmpty(params.note)
exitWithExpectedError('Missing note content')
balena.models.device.note(options.device, params.note)
.nodeify(done)

View File

@ -1,290 +0,0 @@
###
Copyright 2016-2019 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
commandOptions = require('./command-options')
_ = require('lodash')
formatVersion = (v, isRecommended) ->
result = "v#{v}"
if isRecommended
result += ' (recommended)'
return result
resolveVersion = (deviceType, version) ->
if version isnt 'menu'
if version[0] == 'v'
version = version.slice(1)
return Promise.resolve(version)
form = require('resin-cli-form')
balena = require('balena-sdk').fromSharedOptions()
balena.models.os.getSupportedVersions(deviceType)
.then ({ versions, recommended }) ->
choices = versions.map (v) ->
value: v
name: formatVersion(v, v is recommended)
return form.ask
message: 'Select the OS version:'
type: 'list'
choices: choices
default: recommended
exports.versions =
signature: 'os versions <type>'
description: 'show the available balenaOS versions for the given device type'
help: '''
Use this command to show the available balenaOS versions for a certain device type.
Check available types with `balena devices supported`
Example:
$ balena os versions raspberrypi3
'''
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
balena.models.os.getSupportedVersions(params.type)
.then ({ versions, recommended }) ->
versions.forEach (v) ->
console.log(formatVersion(v, v is recommended))
exports.download =
signature: 'os download <type>'
description: 'download an unconfigured os image'
help: '''
Use this command to download an unconfigured os image for a certain device type.
Check available types with `balena devices supported`
If version is not specified the newest stable (non-pre-release) version of OS
is downloaded if available, or the newest version otherwise (if all existing
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.
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 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
'''
permission: 'user'
options: [
{
signature: 'output'
description: 'output path'
parameter: 'output'
alias: 'o'
required: 'You have to specify the output location'
}
commandOptions.osVersionOrSemver
]
action: (params, options, done) ->
Promise = require('bluebird')
unzip = require('unzip2')
fs = require('fs')
rindle = require('rindle')
manager = require('balena-image-manager')
visuals = require('resin-cli-visuals')
console.info("Getting device operating system for #{params.type}")
displayVersion = ''
Promise.try ->
if not options.version
console.warn('OS version is not specified, using the default version:
the newest stable (non-pre-release) version if available,
or the newest version otherwise (if all existing
versions for the given device type are pre-release).')
return 'default'
return resolveVersion(params.type, options.version)
.then (version) ->
if version isnt 'default'
displayVersion = " #{version}"
return manager.get(params.type, version)
.then (stream) ->
bar = new visuals.Progress("Downloading Device OS#{displayVersion}")
spinner = new visuals.Spinner("Downloading Device OS#{displayVersion} (size unknown)")
stream.on 'progress', (state) ->
if state?
bar.update(state)
else
spinner.start()
stream.on 'end', ->
spinner.stop()
# We completely rely on the `mime` custom property
# to make this decision.
# The actual stream should be checked instead.
if stream.mime is 'application/zip'
output = unzip.Extract(path: options.output)
else
output = fs.createWriteStream(options.output)
return rindle.wait(stream.pipe(output)).return(options.output)
.tap (output) ->
console.info('The image was downloaded successfully')
.nodeify(done)
buildConfigForDeviceType = (deviceType, advanced = false) ->
form = require('resin-cli-form')
helpers = require('../utils/helpers')
questions = deviceType.options
if not advanced
advancedGroup = _.find questions,
name: 'advanced'
isGroup: true
if advancedGroup?
override = helpers.getGroupDefaults(advancedGroup)
return form.run(questions, { override })
buildConfig = (image, deviceTypeSlug, advanced = false) ->
Promise = require('bluebird')
helpers = require('../utils/helpers')
Promise.resolve(helpers.getManifest(image, deviceTypeSlug))
.then (deviceTypeManifest) ->
buildConfigForDeviceType(deviceTypeManifest, advanced)
exports.buildConfig =
signature: 'os build-config <image> <device-type>'
description: 'build the OS config and save it to the JSON file'
help: '''
Use this command to prebuild the OS config once and skip the interactive part of `balena os configure`.
Example:
$ balena os build-config ../path/rpi3.img raspberrypi3 --output rpi3-config.json
$ balena os configure ../path/rpi3.img --device 7cf02a6 --config rpi3-config.json
'''
permission: 'user'
options: [
commandOptions.advancedConfig
{
signature: 'output'
description: 'the path to the output JSON file'
alias: 'o'
required: 'the output path is required'
parameter: 'output'
}
]
action: (params, options, done) ->
fs = require('fs')
Promise = require('bluebird')
writeFileAsync = Promise.promisify(fs.writeFile)
buildConfig(params.image, params['device-type'], options.advanced)
.then (answers) ->
writeFileAsync(options.output, JSON.stringify(answers, null, 4))
.nodeify(done)
INIT_WARNING_MESSAGE = '''
Note: Initializing the device may ask for administrative permissions
because we need to access the raw devices directly.
'''
exports.initialize =
signature: 'os initialize <image>'
description: 'initialize an os image'
help: """
Use this command to initialize a device with previously configured operating system image.
#{INIT_WARNING_MESSAGE}
Examples:
$ balena os initialize ../path/rpi.img --type 'raspberry-pi'
"""
permission: 'user'
options: [
commandOptions.yes
{
signature: 'type'
description: 'device type (Check available types with `balena devices supported`)'
parameter: 'type'
alias: 't'
required: 'You have to specify a device type'
}
commandOptions.drive
]
action: (params, options, done) ->
Promise = require('bluebird')
umountAsync = Promise.promisify(require('umount').umount)
form = require('resin-cli-form')
patterns = require('../utils/patterns')
helpers = require('../utils/helpers')
console.info("""
Initializing device
#{INIT_WARNING_MESSAGE}
""")
Promise.resolve(helpers.getManifest(params.image, options.type))
.then (manifest) ->
return manifest.initialization?.options
.then (questions) ->
return form.run questions,
override:
drive: options.drive
.tap (answers) ->
return if not answers.drive?
patterns.confirm(
options.yes
"This will erase #{answers.drive}. Are you sure?"
"Going to erase #{answers.drive}."
true
)
.return(answers.drive)
.then(umountAsync)
.tap (answers) ->
return helpers.sudo([
'internal'
'osinit'
params.image
options.type
JSON.stringify(answers)
])
.then (answers) ->
return if not answers.drive?
# TODO: balena local makes use of ejectAsync, see below
# DO we need this / should we do that here?
# getDrive = (drive) ->
# driveListAsync().then (drives) ->
# selectedDrive = _.find(drives, device: drive)
# if not selectedDrive?
# throw new Error("Drive not found: #{drive}")
# return selectedDrive
# if (os.platform() is 'win32') and selectedDrive.mountpoint?
# ejectAsync = Promise.promisify(require('removedrive').eject)
# return ejectAsync(selectedDrive.mountpoint)
umountAsync(answers.drive).tap ->
console.info("You can safely remove #{answers.drive} now")
.nodeify(done)

353
lib/actions/os.js Normal file
View File

@ -0,0 +1,353 @@
/*
Copyright 2016-2020 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as commandOptions from './command-options';
import * as _ from 'lodash';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
const formatVersion = function (v, isRecommended) {
let result = `v${v}`;
if (isRecommended) {
result += ' (recommended)';
}
return result;
};
const resolveVersion = function (deviceType, version) {
if (version !== 'menu') {
if (version[0] === 'v') {
version = version.slice(1);
}
return Promise.resolve(version);
}
const form = require('resin-cli-form');
const balena = getBalenaSdk();
return balena.models.os
.getSupportedVersions(deviceType)
.then(function ({ versions: vs, recommended }) {
const choices = vs.map((v) => ({
value: v,
name: formatVersion(v, v === recommended),
}));
return form.ask({
message: 'Select the OS version:',
type: 'list',
choices,
default: recommended,
});
});
};
export const versions = {
signature: 'os versions <type>',
description: 'show the available balenaOS versions for the given device type',
help: `\
Use this command to show the available balenaOS versions for a certain device type.
Check available types with \`balena devices supported\`
Example:
$ balena os versions raspberrypi3\
`,
action(params) {
const balena = getBalenaSdk();
return balena.models.os
.getSupportedVersions(params.type)
.then(({ versions: vs, recommended }) => {
vs.forEach((v) => {
console.log(formatVersion(v, v === recommended));
});
});
},
};
export const download = {
signature: 'os download <type>',
description: 'download an unconfigured os image',
help: `\
Use this command to download an unconfigured os image for a certain device type.
Check available types with \`balena devices supported\`
If version is not specified the newest stable (non-pre-release) version of OS
is downloaded if available, or the newest version otherwise (if all existing
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.
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 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\
`,
permission: 'user',
options: [
{
signature: 'output',
description: 'output path',
parameter: 'output',
alias: 'o',
required: 'You have to specify the output location',
},
commandOptions.osVersionOrSemver,
],
action(params, options) {
const Promise = require('bluebird');
const unzip = require('node-unzip-2');
const fs = require('fs');
const rindle = require('rindle');
const manager = require('balena-image-manager');
console.info(`Getting device operating system for ${params.type}`);
let displayVersion = '';
return Promise.try(function () {
if (!options.version) {
console.warn(`OS version is not specified, using the default version: \
the newest stable (non-pre-release) version if available, \
or the newest version otherwise (if all existing \
versions for the given device type are pre-release).`);
return 'default';
}
return resolveVersion(params.type, options.version);
})
.then(function (version) {
if (version !== 'default') {
displayVersion = ` ${version}`;
}
return manager.get(params.type, version);
})
.then(function (stream) {
const visuals = getVisuals();
const bar = new visuals.Progress(
`Downloading Device OS${displayVersion}`,
);
const spinner = new visuals.Spinner(
`Downloading Device OS${displayVersion} (size unknown)`,
);
stream.on('progress', function (state) {
if (state != null) {
return bar.update(state);
} else {
return spinner.start();
}
});
stream.on('end', () => {
spinner.stop();
});
// We completely rely on the `mime` custom property
// to make this decision.
// The actual stream should be checked instead.
let output;
if (stream.mime === 'application/zip') {
output = unzip.Extract({ path: options.output });
} else {
output = fs.createWriteStream(options.output);
}
return rindle.wait(stream.pipe(output)).return(options.output);
})
.tap(() => {
console.info('The image was downloaded successfully');
});
},
};
const buildConfigForDeviceType = function (deviceType, advanced) {
if (advanced == null) {
advanced = false;
}
const form = require('resin-cli-form');
const helpers = require('../utils/helpers');
let override;
const questions = deviceType.options;
if (!advanced) {
const advancedGroup = _.find(questions, {
name: 'advanced',
isGroup: true,
});
if (advancedGroup != null) {
override = helpers.getGroupDefaults(advancedGroup);
}
}
return form.run(questions, { override });
};
const $buildConfig = function (image, deviceTypeSlug, advanced) {
if (advanced == null) {
advanced = false;
}
const Promise = require('bluebird');
const helpers = require('../utils/helpers');
return Promise.resolve(
helpers.getManifest(image, deviceTypeSlug),
).then((deviceTypeManifest) =>
buildConfigForDeviceType(deviceTypeManifest, advanced),
);
};
export const buildConfig = {
signature: 'os build-config <image> <device-type>',
description: 'build the OS config and save it to the JSON file',
help: `\
Use this command to prebuild the OS config once and skip the interactive part of \`balena os configure\`.
Example:
$ balena os build-config ../path/rpi3.img raspberrypi3 --output rpi3-config.json
$ balena os configure ../path/rpi3.img --device 7cf02a6 --config rpi3-config.json\
`,
permission: 'user',
options: [
commandOptions.advancedConfig,
{
signature: 'output',
description: 'the path to the output JSON file',
alias: 'o',
required: 'the output path is required',
parameter: 'output',
},
],
action(params, options) {
const fs = require('fs');
const Promise = require('bluebird');
const writeFileAsync = Promise.promisify(fs.writeFile);
return $buildConfig(
params.image,
params['device-type'],
options.advanced,
).then((answers) =>
writeFileAsync(options.output, JSON.stringify(answers, null, 4)),
);
},
};
const INIT_WARNING_MESSAGE = `\
Note: Initializing the device may ask for administrative permissions
because we need to access the raw devices directly.\
`;
export const initialize = {
signature: 'os initialize <image>',
description: 'initialize an os image',
help: `\
Use this command to initialize a device with previously configured operating system image.
${INIT_WARNING_MESSAGE}
Examples:
$ balena os initialize ../path/rpi.img --type 'raspberry-pi'\
`,
permission: 'user',
options: [
commandOptions.yes,
{
signature: 'type',
description:
'device type (Check available types with `balena devices supported`)',
parameter: 'type',
alias: 't',
required: 'You have to specify a device type',
},
commandOptions.drive,
],
action(params, options) {
const Promise = require('bluebird');
const umountAsync = Promise.promisify(require('umount').umount);
const form = require('resin-cli-form');
const patterns = require('../utils/patterns');
const helpers = require('../utils/helpers');
console.info(`\
Initializing device
${INIT_WARNING_MESSAGE}\
`);
return Promise.resolve(helpers.getManifest(params.image, options.type))
.then((manifest) =>
form.run(manifest.initialization?.options, {
override: {
drive: options.drive,
},
}),
)
.tap(function (answers) {
if (answers.drive == null) {
return;
}
return patterns
.confirm(
options.yes,
`This will erase ${answers.drive}. Are you sure?`,
`Going to erase ${answers.drive}.`,
true,
)
.return(answers.drive)
.then(umountAsync);
})
.tap((answers) =>
helpers.sudo([
'internal',
'osinit',
params.image,
options.type,
JSON.stringify(answers),
]),
)
.then(function (answers) {
if (answers.drive == null) {
return;
}
// TODO: balena local makes use of ejectAsync, see below
// DO we need this / should we do that here?
// getDrive = (drive) ->
// driveListAsync().then (drives) ->
// selectedDrive = _.find(drives, device: drive)
// if not selectedDrive?
// throw new Error("Drive not found: #{drive}")
// return selectedDrive
// if (os.platform() is 'win32') and selectedDrive.mountpoint?
// ejectAsync = Promise.promisify(require('removedrive').eject)
// return ejectAsync(selectedDrive.mountpoint)
return umountAsync(answers.drive).tap(() => {
console.info(`You can safely remove ${answers.drive} now`);
});
});
},
};

View File

@ -1,337 +0,0 @@
###
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
_ = require('lodash')
dockerUtils = require('../utils/docker')
allDeviceTypes = undefined
isCurrent = (commit) ->
return commit == 'latest' or commit == 'current'
getDeviceTypes = ->
Bluebird = require('bluebird')
_ = require('lodash')
if allDeviceTypes != undefined
return Bluebird.resolve(allDeviceTypes)
balena = require('balena-sdk').fromSharedOptions()
balena.models.config.getDeviceTypes()
.then (deviceTypes) ->
_.sortBy(deviceTypes, 'name')
.tap (dt) ->
allDeviceTypes = dt
getDeviceTypesWithSameArch = (deviceTypeSlug) ->
_ = require('lodash')
getDeviceTypes()
.then (deviceTypes) ->
deviceType = _.find(deviceTypes, slug: deviceTypeSlug)
_(deviceTypes).filter(arch: deviceType.arch).map('slug').value()
getApplicationsWithSuccessfulBuilds = (deviceType) ->
preload = require('balena-preload')
balena = require('balena-sdk').fromSharedOptions()
getDeviceTypesWithSameArch(deviceType)
.then (deviceTypes) ->
balena.pine.get
resource: 'my_application'
options:
$filter:
device_type:
$in: deviceTypes
owns__release:
$any:
$alias: 'r'
$expr:
r:
status: 'success'
$expand: preload.applicationExpandOptions
$select: [ 'id', 'app_name', 'device_type', 'commit', 'should_track_latest_release' ]
$orderby: 'app_name asc'
selectApplication = (deviceType) ->
visuals = require('resin-cli-visuals')
form = require('resin-cli-form')
{ exitWithExpectedError } = require('../utils/patterns')
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and releases.')
applicationInfoSpinner.start()
getApplicationsWithSuccessfulBuilds(deviceType)
.then (applications) ->
applicationInfoSpinner.stop()
if applications.length == 0
exitWithExpectedError("You have no apps with successful releases for a '#{deviceType}' device type.")
form.ask
message: 'Select an application'
type: 'list'
choices: applications.map (app) ->
name: app.app_name
value: app
selectApplicationCommit = (releases) ->
form = require('resin-cli-form')
{ exitWithExpectedError } = require('../utils/patterns')
if releases.length == 0
exitWithExpectedError('This application has no successful releases.')
DEFAULT_CHOICE = { 'name': 'current', 'value': 'current' }
choices = [ DEFAULT_CHOICE ].concat releases.map (release) ->
name: "#{release.end_timestamp} - #{release.commit}"
value: release.commit
return form.ask
message: 'Select a release'
type: 'list'
default: 'current'
choices: choices
offerToDisableAutomaticUpdates = (application, commit, pinDevice) ->
Promise = require('bluebird')
balena = require('balena-sdk').fromSharedOptions()
form = require('resin-cli-form')
if isCurrent(commit) or not application.should_track_latest_release or pinDevice
return Promise.resolve()
message = '''
This application is set to automatically update all devices to the current version.
This might be unexpected behaviour: with this enabled, the preloaded device will still
download and install the current release once it is online.
Do you want to disable automatic updates for this application?
Warning: To re-enable this requires direct api calls,
see https://balena.io/docs/reference/api/resources/device/#set-device-to-release
Alternatively you can pass the --pin-device-to-release flag to pin only this device to the selected release.
'''
form.ask
message: message,
type: 'confirm'
.then (update) ->
if not update
return
balena.pine.patch
resource: 'application'
id: application.id
body:
should_track_latest_release: false
preloadOptions = dockerUtils.appendConnectionOptions [
{
signature: 'app'
parameter: 'appId'
description: 'id of the application to preload'
alias: 'a'
}
{
signature: 'commit'
parameter: 'hash'
description: '''
The commit hash for a specific application release to preload, use "current" to specify the current
release (ignored if no appId is given). The current release is usually also the latest, but can be
manually pinned using https://github.com/balena-io-projects/staged-releases .
'''
alias: 'c'
}
{
signature: 'splash-image'
parameter: 'splashImage.png'
description: 'path to a png image to replace the splash screen'
alias: 's'
}
{
signature: 'dont-check-arch'
boolean: true
description: 'Disables check for matching architecture in image and application'
}
{
signature: 'pin-device-to-release'
boolean: true
description: 'Pin the preloaded device to the preloaded release on provision'
alias: 'p'
}
{
signature: 'add-certificate'
parameter: 'certificate.crt'
description: '''
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
The file name must end with '.crt' and must not be already contained in the preloader's
/etc/ssl/certs folder.
Can be repeated to add multiple certificates.
'''
}
]
# Remove dockerPort `-p` alias as it conflicts with pin-device-to-release
delete _.find(preloadOptions, signature: 'dockerPort').alias
module.exports =
signature: 'preload <image>'
description: 'preload an app on a disk image (or Edison zip archive)'
help: '''
Preload a balena application release (app images/containers), and optionally
a balenaOS splash screen, in a previously downloaded balenaOS image file (or
Edison zip archive) in the local disk. The balenaOS image file can then be
flashed to a device's SD card. When the device boots, it will not need to
download the application, as it was preloaded.
Warning: "balena preload" requires Docker to be correctly installed in
your shell environment. For more information (including Windows support)
check: https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
Examples:
$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png
$ balena preload balena.img
'''
permission: 'user'
primary: true
options: preloadOptions
action: (params, options, done) ->
_ = require('lodash')
Promise = require('bluebird')
balena = require('balena-sdk').fromSharedOptions()
preload = require('balena-preload')
visuals = require('resin-cli-visuals')
nodeCleanup = require('node-cleanup')
{ exitWithExpectedError } = require('../utils/patterns')
progressBars = {}
progressHandler = (event) ->
progressBar = progressBars[event.name]
if not progressBar
progressBar = progressBars[event.name] = new visuals.Progress(event.name)
progressBar.update(percentage: event.percentage)
spinners = {}
spinnerHandler = (event) ->
spinner = spinners[event.name]
if not spinner
spinner = spinners[event.name] = new visuals.Spinner(event.name)
if event.action == 'start'
spinner.start()
else
console.log()
spinner.stop()
options.commit = if isCurrent(options.commit) then 'latest' else options.commit
options.image = params.image
options.appId = options.app
delete options.app
options.splashImage = options['splash-image']
delete options['splash-image']
options.dontCheckArch = options['dont-check-arch'] || false
delete options['dont-check-arch']
if options.dontCheckArch and not options.appId
exitWithExpectedError('You need to specify an app id if you disable the architecture check.')
options.pinDevice = options['pin-device-to-release'] || false
delete options['pin-device-to-release']
if _.isArray(options['add-certificate'])
certificates = options['add-certificate']
else if options['add-certificate'] == undefined
certificates = []
else
certificates = [ options['add-certificate'] ]
for certificate in certificates
if not certificate.endsWith('.crt')
exitWithExpectedError('Certificate file name must end with ".crt"')
# Get a configured dockerode instance
dockerUtils.getDocker(options)
.then (docker) ->
preloader = new preload.Preloader(
balena
docker
options.appId
options.commit
options.image
options.splashImage
options.proxy
options.dontCheckArch
options.pinDevice
certificates
)
gotSignal = false
nodeCleanup (exitCode, signal) ->
if signal
gotSignal = true
nodeCleanup.uninstall() # don't call cleanup handler again
preloader.cleanup()
.then ->
# calling process.exit() won't inform parent process of signal
process.kill(process.pid, signal)
return false
if process.env.DEBUG
preloader.stderr.pipe(process.stderr)
preloader.on('progress', progressHandler)
preloader.on('spinner', spinnerHandler)
return new Promise (resolve, reject) ->
preloader.on('error', reject)
preloader.prepare()
.then ->
# If no appId was provided, show a list of matching apps
Promise.try ->
if not preloader.appId
selectApplication(preloader.config.deviceType)
.then (application) ->
preloader.setApplication(application)
.then ->
# Use the commit given as --commit or show an interactive commit selection menu
Promise.try ->
if options.commit
if isCurrent(options.commit) and preloader.application.commit
# handle `--commit current` (and its `--commit latest` synonym)
return 'latest'
release = _.find preloader.application.owns__release, (release) ->
release.commit.startsWith(options.commit)
if not release
exitWithExpectedError('There is no release matching this commit')
return release.commit
selectApplicationCommit(preloader.application.owns__release)
.then (commit) ->
if isCurrent(commit)
preloader.commit = preloader.application.commit
else
preloader.commit = commit
# Propose to disable automatic app updates if the commit is not the current release
offerToDisableAutomaticUpdates(preloader.application, commit, options.pinDevice)
.then ->
# All options are ready: preload the image.
preloader.preload()
.catch(balena.errors.BalenaError, exitWithExpectedError)
.then(resolve)
.catch(reject)
.then(done)
.finally ->
if not gotSignal
preloader.cleanup()

423
lib/actions/preload.js Normal file
View File

@ -0,0 +1,423 @@
/*
Copyright 2016-2020 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as _ from 'lodash';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
import * as dockerUtils from '../utils/docker';
const isCurrent = (commit) => commit === 'latest' || commit === 'current';
let allDeviceTypes;
const getDeviceTypes = function () {
const Bluebird = require('bluebird');
if (allDeviceTypes !== undefined) {
return Bluebird.resolve(allDeviceTypes);
}
const balena = getBalenaSdk();
return balena.models.config
.getDeviceTypes()
.then((deviceTypes) => _.sortBy(deviceTypes, 'name'))
.tap((dt) => {
allDeviceTypes = dt;
});
};
const getDeviceTypesWithSameArch = function (deviceTypeSlug) {
return getDeviceTypes().then(function (deviceTypes) {
const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug });
return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value();
});
};
const getApplicationsWithSuccessfulBuilds = function (deviceType) {
const balenaPreload = require('balena-preload');
const balena = getBalenaSdk();
return getDeviceTypesWithSameArch(deviceType).then((deviceTypes) => {
/** @type {import('balena-sdk').PineOptionsFor<import('balena-sdk').Application>} */
const options = {
$filter: {
device_type: {
$in: deviceTypes,
},
owns__release: {
$any: {
$alias: 'r',
$expr: {
r: {
status: 'success',
},
},
},
},
},
$expand: balenaPreload.applicationExpandOptions,
$select: [
'id',
'app_name',
'device_type',
'commit',
'should_track_latest_release',
],
$orderby: 'app_name asc',
};
return balena.pine.get({
resource: 'my_application',
options,
});
});
};
const selectApplication = function (deviceType) {
const visuals = getVisuals();
const form = require('resin-cli-form');
const { exitWithExpectedError } = require('../errors');
const applicationInfoSpinner = new visuals.Spinner(
'Downloading list of applications and releases.',
);
applicationInfoSpinner.start();
return getApplicationsWithSuccessfulBuilds(deviceType).then(function (
applications,
) {
applicationInfoSpinner.stop();
if (applications.length === 0) {
exitWithExpectedError(
`You have no apps with successful releases for a '${deviceType}' device type.`,
);
}
return form.ask({
message: 'Select an application',
type: 'list',
choices: applications.map((app) => ({
name: app.app_name,
value: app,
})),
});
});
};
const selectApplicationCommit = function (releases) {
const form = require('resin-cli-form');
const { exitWithExpectedError } = require('../errors');
if (releases.length === 0) {
exitWithExpectedError('This application has no successful releases.');
}
const DEFAULT_CHOICE = { name: 'current', value: 'current' };
const choices = [DEFAULT_CHOICE].concat(
releases.map((release) => ({
name: `${release.end_timestamp} - ${release.commit}`,
value: release.commit,
})),
);
return form.ask({
message: 'Select a release',
type: 'list',
default: 'current',
choices,
});
};
const offerToDisableAutomaticUpdates = function (
application,
commit,
pinDevice,
) {
const Promise = require('bluebird');
const balena = getBalenaSdk();
const form = require('resin-cli-form');
if (
isCurrent(commit) ||
!application.should_track_latest_release ||
pinDevice
) {
return Promise.resolve();
}
const message = `\
This application is set to automatically update all devices to the current version.
This might be unexpected behavior: with this enabled, the preloaded device will still
download and install the current release once it is online.
Do you want to disable automatic updates for this application?
Warning: To re-enable this requires direct api calls,
see https://balena.io/docs/reference/api/resources/device/#set-device-to-release
Alternatively you can pass the --pin-device-to-release flag to pin only this device to the selected release.\
`;
return form
.ask({
message,
type: 'confirm',
})
.then(function (update) {
if (!update) {
return;
}
return balena.pine.patch({
resource: 'application',
id: application.id,
body: {
should_track_latest_release: false,
},
});
});
};
const preloadOptions = dockerUtils.appendConnectionOptions([
{
signature: 'app',
parameter: 'appId',
description: 'id of the application to preload',
alias: 'a',
},
{
signature: 'commit',
parameter: 'hash',
description: `\
The commit hash for a specific application release to preload, use "current" to specify the current
release (ignored if no appId is given). The current release is usually also the latest, but can be
manually pinned using https://github.com/balena-io-projects/staged-releases .\
`,
alias: 'c',
},
{
signature: 'splash-image',
parameter: 'splashImage.png',
description: 'path to a png image to replace the splash screen',
alias: 's',
},
{
signature: 'dont-check-arch',
boolean: true,
description:
'Disables check for matching architecture in image and application',
},
{
signature: 'pin-device-to-release',
boolean: true,
description:
'Pin the preloaded device to the preloaded release on provision',
alias: 'p',
},
{
signature: 'add-certificate',
parameter: 'certificate.crt',
description: `\
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
The file name must end with '.crt' and must not be already contained in the preloader's
/etc/ssl/certs folder.
Can be repeated to add multiple certificates.\
`,
},
]);
// Remove dockerPort `-p` alias as it conflicts with pin-device-to-release
delete _.find(preloadOptions, { signature: 'dockerPort' }).alias;
export const preload = {
signature: 'preload <image>',
description: 'preload an app on a disk image (or Edison zip archive)',
help: `\
Preload a balena application release (app images/containers), and optionally
a balenaOS splash screen, in a previously downloaded '.img' balenaOS image file
in the local disk (a zip file is only accepted for the Intel Edison device type).
After preloading, the balenaOS image file can be flashed to a device's SD card.
When the device boots, it will not need to download the application, as it was
preloaded.
Warning: "balena preload" requires Docker to be correctly installed in
your shell environment. For more information (including Windows support)
check: https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
Examples:
$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png
$ balena preload balena.img\
`,
permission: 'user',
primary: true,
options: preloadOptions,
action(params, options, done) {
let certificates;
const Promise = require('bluebird');
const balena = getBalenaSdk();
const balenaPreload = require('balena-preload');
const visuals = getVisuals();
const nodeCleanup = require('node-cleanup');
const { exitWithExpectedError } = require('../errors');
const progressBars = {};
const progressHandler = function (event) {
let progressBar = progressBars[event.name];
if (!progressBar) {
progressBar = progressBars[event.name] = new visuals.Progress(
event.name,
);
}
return progressBar.update({ percentage: event.percentage });
};
const spinners = {};
const spinnerHandler = function (event) {
let spinner = spinners[event.name];
if (!spinner) {
spinner = spinners[event.name] = new visuals.Spinner(event.name);
}
if (event.action === 'start') {
return spinner.start();
} else {
console.log();
return spinner.stop();
}
};
options.commit = isCurrent(options.commit) ? 'latest' : options.commit;
options.image = params.image;
options.appId = options.app;
delete options.app;
options.splashImage = options['splash-image'];
delete options['splash-image'];
options.dontCheckArch = options['dont-check-arch'] || false;
delete options['dont-check-arch'];
if (options.dontCheckArch && !options.appId) {
exitWithExpectedError(
'You need to specify an app id if you disable the architecture check.',
);
}
options.pinDevice = options['pin-device-to-release'] || false;
delete options['pin-device-to-release'];
if (Array.isArray(options['add-certificate'])) {
certificates = options['add-certificate'];
} else if (options['add-certificate'] === undefined) {
certificates = [];
} else {
certificates = [options['add-certificate']];
}
for (let certificate of certificates) {
if (!certificate.endsWith('.crt')) {
exitWithExpectedError('Certificate file name must end with ".crt"');
}
}
// Get a configured dockerode instance
return dockerUtils.getDocker(options).then(function (docker) {
const preloader = new balenaPreload.Preloader(
balena,
docker,
options.appId,
options.commit,
options.image,
options.splashImage,
options.proxy,
options.dontCheckArch,
options.pinDevice,
certificates,
);
let gotSignal = false;
nodeCleanup(function (_exitCode, signal) {
if (signal) {
gotSignal = true;
nodeCleanup.uninstall(); // don't call cleanup handler again
preloader.cleanup().then(() => {
// calling process.exit() won't inform parent process of signal
process.kill(process.pid, signal);
});
return false;
}
});
if (process.env.DEBUG) {
preloader.stderr.pipe(process.stderr);
}
preloader.on('progress', progressHandler);
preloader.on('spinner', spinnerHandler);
return new Promise(function (resolve, reject) {
preloader.on('error', reject);
return preloader
.prepare()
.then(() => {
// If no appId was provided, show a list of matching apps
if (!preloader.appId) {
return selectApplication(
preloader.config.deviceType,
).then((application) => preloader.setApplication(application));
}
})
.then(() => {
// Use the commit given as --commit or show an interactive commit selection menu
if (options.commit) {
if (isCurrent(options.commit) && preloader.application.commit) {
// handle `--commit current` (and its `--commit latest` synonym)
return 'latest';
}
const release = _.find(preloader.application.owns__release, (r) =>
r.commit.startsWith(options.commit),
);
if (!release) {
exitWithExpectedError(
'There is no release matching this commit',
);
}
return release.commit;
}
return selectApplicationCommit(preloader.application.owns__release);
})
.then(function (commit) {
if (isCurrent(commit)) {
preloader.commit = preloader.application.commit;
} else {
preloader.commit = commit;
}
// Propose to disable automatic app updates if the commit is not the current release
return offerToDisableAutomaticUpdates(
preloader.application,
commit,
options.pinDevice,
);
})
.then(() =>
// All options are ready: preload the image.
preloader.preload(),
)
.catch(balena.errors.BalenaError, exitWithExpectedError)
.then(resolve)
.catch(reject);
})
.then(done)
.finally(function () {
if (!gotSignal) {
return preloader.cleanup();
}
});
});
},
};

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2019 Balena Ltd.
Copyright 2016-2020 Balena Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,12 +18,15 @@ import { BalenaSDK } from 'balena-sdk';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { registrySecretsHelp } from '../utils/messages';
import { ExpectedError } from '../errors';
import { getBalenaSdk } from '../utils/lazy';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import {
validateApplicationName,
validateDotLocalUrl,
validateIPAddress,
} from '../utils/validation';
import { isV12 } from '../utils/version';
enum BuildTarget {
Cloud,
@ -44,9 +47,7 @@ function getBuildTarget(appOrDevice: string): BuildTarget | null {
}
async function getAppOwner(sdk: BalenaSDK, appName: string) {
const { exitWithExpectedError, selectFromList } = await import(
'../utils/patterns'
);
const { selectFromList } = await import('../utils/patterns');
const _ = await import('lodash');
const applications = await sdk.models.application.getAll({
@ -62,7 +63,7 @@ async function getAppOwner(sdk: BalenaSDK, appName: string) {
});
if (applications == null || applications.length === 0) {
exitWithExpectedError(
throw new ExpectedError(
stripIndent`
No applications found with name: ${appName}.
@ -79,7 +80,7 @@ async function getAppOwner(sdk: BalenaSDK, appName: string) {
// user has access to a collab app with the same name as a personal app. We
// present a list to the user which shows the fully qualified application
// name (user/appname) and allows them to select
const entries = _.map(applications, app => {
const entries = _.map(applications, (app) => {
const username = _.get(app, 'user[0].username');
return {
name: `${username}/${appName}`,
@ -88,9 +89,7 @@ async function getAppOwner(sdk: BalenaSDK, appName: string) {
});
const selected = await selectFromList(
`${
entries.length
} applications found with that name, please select the application you would like to push to`,
`${entries.length} applications found with that name, please select the application you would like to push to`,
entries,
);
@ -109,12 +108,16 @@ export const push: CommandDefinition<
emulated?: boolean;
dockerfile?: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
nocache?: boolean;
'noparent-check'?: boolean;
'registry-secrets'?: string;
nogitignore?: boolean;
nolive?: boolean;
detached?: boolean;
service?: string | string[];
system?: boolean;
env?: string | string[];
'convert-eol'?: boolean;
'noconvert-eol'?: boolean;
}
> = {
signature: 'push <applicationOrDevice>',
@ -142,12 +145,14 @@ export const push: CommandDefinition<
When pushing to a local device a live session will be started.
The project source folder is watched for filesystem events, and changes
to files and folders are automatically synchronized to the running
containers. The synchronisation is only in one direction, from this machine to
containers. The synchronization is only in one direction, from this machine to
the device, and changes made on the device itself may be overwritten.
This feature requires a device running supervisor version v9.7.0 or greater.
${registrySecretsHelp.split('\n').join('\n\t\t')}
${dockerignoreHelp.split('\n').join('\n\t\t')}
Examples:
$ balena push myApp
@ -168,7 +173,7 @@ export const push: CommandDefinition<
signature: 'source',
alias: 's',
description:
'The source that should be sent to the balena builder to be built (defaults to the current directory)',
'Source directory to be sent to balenaCloud or balenaOS device (default: current working dir)',
parameter: 'source',
},
{
@ -189,6 +194,12 @@ export const push: CommandDefinition<
description: "Don't use cache when building this project",
boolean: true,
},
{
signature: 'noparent-check',
description:
"Disable project validation check of 'docker-compose.yml' file in parent folder",
boolean: true,
},
{
signature: 'registry-secrets',
alias: 'R',
@ -203,7 +214,7 @@ export const push: CommandDefinition<
boolean: true,
description: stripIndent`
Don't run a live session on this push. The filesystem will not be monitored, and changes
will not be synchronised to any running containers. Note that both this flag and --detached
will not be synchronized to any running containers. Note that both this flag and --detached
and required to cause the process to end once the initial build has completed.`,
},
{
@ -245,25 +256,52 @@ export const push: CommandDefinition<
left hand side of the = character will be treated as the variable name.
`,
},
{
signature: 'convert-eol',
alias: 'l',
description: isV12()
? 'No-op and deprecated since balena CLI v12.0.0'
: stripIndent`
On Windows only, convert line endings from CRLF (Windows format) to LF (Unix format).
Source files are not modified.`,
boolean: true,
},
...(isV12()
? [
{
signature: 'noconvert-eol',
description:
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).",
boolean: true,
},
]
: []),
{
signature: 'nogitignore',
alias: 'G',
description: isV12()
? 'No-op and deprecated since balena CLI v12.0.0. See "balena help push".'
: stripIndent`
Disregard all .gitignore files, and consider only the .dockerignore file (if any)
at the source directory. This will be the default behavior in an upcoming major
version release. For more information, see 'balena help push'.
`,
boolean: true,
},
],
async action(params, options, done) {
const sdk = (await import('balena-sdk')).fromSharedOptions();
async action(params, options) {
const sdk = getBalenaSdk();
const Bluebird = await import('bluebird');
const isArray = await import('lodash/isArray');
const remote = await import('../utils/remote-build');
const deviceDeploy = await import('../utils/device/deploy');
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
'../utils/patterns'
);
const { validateSpecifiedDockerfile, getRegistrySecrets } = await import(
'../utils/compose_ts'
);
const { checkLoggedIn } = await import('../utils/patterns');
const { validateProjectDirectory } = await import('../utils/compose_ts');
const { BuildError } = await import('../utils/device/errors');
const appOrDevice: string | null =
params.applicationOrDevice_raw || params.applicationOrDevice;
if (appOrDevice == null) {
exitWithExpectedError('You must specify an application or a device');
throw new ExpectedError('You must specify an application or a device');
}
const source = options.source || '.';
@ -271,43 +309,48 @@ export const push: CommandDefinition<
console.error(`[debug] Using ${source} as build source`);
}
const dockerfilePath = validateSpecifiedDockerfile(
source,
options.dockerfile,
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
sdk,
{
dockerfilePath: options.dockerfile,
noParentCheck: options['noparent-check'] || false,
projectPath: source,
registrySecretsPath: options['registry-secrets'],
},
);
const registrySecrets = await getRegistrySecrets(
sdk,
options['registry-secrets'],
);
const nogitignore = !!options.nogitignore || isV12();
const convertEol = isV12()
? !options['noconvert-eol']
: !!options['convert-eol'];
const buildTarget = getBuildTarget(appOrDevice);
switch (buildTarget) {
case BuildTarget.Cloud:
// Ensure that the live argument has not been passed to a cloud build
if (options.nolive != null) {
exitWithExpectedError(
throw new ExpectedError(
'The --nolive flag is only valid when pushing to a local mode device',
);
}
if (options.service) {
exitWithExpectedError(
throw new ExpectedError(
'The --service flag is only valid when pushing to a local mode device.',
);
}
if (options.system) {
exitWithExpectedError(
throw new ExpectedError(
'The --system flag is only valid when pushing to a local mode device.',
);
}
if (options.env) {
exitWithExpectedError(
throw new ExpectedError(
'The --env flag is only valid when pushing to a local mode device.',
);
}
const app = appOrDevice;
await exitIfNotLoggedIn();
await checkLoggedIn();
await Bluebird.join(
sdk.auth.getToken(),
sdk.settings.get('balenaUrl'),
@ -319,6 +362,7 @@ export const push: CommandDefinition<
nocache: options.nocache || false,
registrySecrets,
headless: options.detached || false,
convertEol,
};
const args = {
app,
@ -326,19 +370,19 @@ export const push: CommandDefinition<
source,
auth: token,
baseUrl,
nogitignore,
sdk,
opts,
};
return await remote.startRemoteBuild(args);
},
).nodeify(done);
);
break;
case BuildTarget.Device:
const device = appOrDevice;
const servicesToDisplay =
options.service != null
? isArray(options.service)
? Array.isArray(options.service)
? options.service
: [options.service]
: undefined;
@ -350,6 +394,8 @@ export const push: CommandDefinition<
dockerfilePath,
registrySecrets,
nocache: options.nocache || false,
nogitignore,
noParentCheck: options['noparent-check'] || false,
nolive: options.nolive || false,
detached: options.detached || false,
services: servicesToDisplay,
@ -358,24 +404,22 @@ export const push: CommandDefinition<
typeof options.env === 'string'
? [options.env]
: options.env || [],
convertEol,
}),
)
.catch(BuildError, e => {
exitWithExpectedError(e.toString());
})
.nodeify(done);
).catch(BuildError, (e) => {
throw new ExpectedError(e.toString());
});
break;
default:
exitWithExpectedError(
throw new ExpectedError(
stripIndent`
Build target not recognised. Please provide either an application name or device address.
Build target not recognized. Please provide either an application name or device address.
The only supported device addresses currently are IP addresses.
If you believe your build target should have been detected, and this is an error, please
create an issue.`,
);
break;
}
},
};

View File

@ -1,100 +0,0 @@
###
Copyright 2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
dockerInfoProperties = [
'Containers'
'ContainersRunning'
'ContainersPaused'
'ContainersStopped'
'Images'
'Driver'
'SystemTime'
'KernelVersion'
'OperatingSystem'
'Architecture'
]
dockerVersionProperties = [
'Version'
'ApiVersion'
]
module.exports =
signature: 'scan'
description: 'Scan for balenaOS devices in your local network'
help: '''
Examples:
$ balena scan
$ balena scan --timeout 120
$ balena scan --verbose
'''
options: [
signature: 'verbose'
boolean: true
description: 'Display full info'
alias: 'v'
,
signature: 'timeout'
parameter: 'timeout'
description: 'Scan timeout in seconds'
alias: 't'
]
primary: true
root: true
action: (params, options, done) ->
Promise = require('bluebird')
_ = require('lodash')
prettyjson = require('prettyjson')
{ discover } = require('balena-sync')
{ SpinnerPromise } = require('resin-cli-visuals')
{ dockerPort, dockerTimeout } = require('./local/common')
dockerUtils = require('../utils/docker')
{ exitWithExpectedError } = require('../utils/patterns')
if options.timeout?
options.timeout *= 1000
Promise.try ->
new SpinnerPromise
promise: discover.discoverLocalBalenaOsDevices(options.timeout)
startMessage: 'Scanning for local balenaOS devices..'
stopMessage: 'Reporting scan results'
.filter ({ address }) ->
Promise.try ->
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
docker.pingAsync()
.return(true)
.catchReturn(false)
.tap (devices) ->
if _.isEmpty(devices)
exitWithExpectedError('Could not find any balenaOS devices in the local network')
.map ({ host, address }) ->
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
Promise.props
dockerInfo: docker.infoAsync().catchReturn('Could not get Docker info')
dockerVersion: docker.versionAsync().catchReturn('Could not get Docker version')
.then ({ dockerInfo, dockerVersion }) ->
if not options.verbose
dockerInfo = _.pick(dockerInfo, dockerInfoProperties) if _.isObject(dockerInfo)
dockerVersion = _.pick(dockerVersion, dockerVersionProperties) if _.isObject(dockerVersion)
return { host, address, dockerInfo, dockerVersion }
.then (devicesInfo) ->
console.log(prettyjson.render(devicesInfo, noColor: true))
.nodeify(done)

View File

@ -1,39 +0,0 @@
/*
Copyright 2016-2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { CommandDefinition } from 'capitano';
export const list: CommandDefinition = {
signature: 'settings',
description: 'print current settings',
help: `\
Use this command to display detected settings
Examples:
$ balena settings\
`,
async action(_params, _options, done) {
const balena = (await import('balena-sdk')).fromSharedOptions();
const prettyjson = await import('prettyjson');
return balena.settings
.getAll()
.then(prettyjson.render)
.then(console.log)
.nodeify(done);
},
};

Some files were not shown because too many files have changed in this diff Show More