Compare commits

...

207 Commits

Author SHA1 Message Date
fed4328dec Produce AppImage assets
Change-type: minor
2021-04-21 23:44:39 +01:00
b4495839ca v12.44.11 2021-04-21 16:46:55 +03:00
f45ac42dd3 Merge pull request #2255 from balena-io/build-args-warning
Add message regarding deprecation of --buildArg option
2021-04-21 13:44:15 +00:00
fa26004648 Add message regarding deprecation of --buildArg option in build/deploy commands
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-04-21 07:26:57 +02:00
ba1ea54d69 v12.44.10 2021-04-16 01:24:55 +03:00
9fb62d92b7 Merge pull request #2253 from balena-io/2252-fix-ssh-service-list
ssh: fix incorrect service name parsing in local mode
2021-04-15 22:21:58 +00:00
8780a24fb5 ssh: fix incorrect service name parsing in local mode
Resolves: #2252
Change-type: patch
Signed-off-by: Tomás Migone <tomas@balena.io>
2021-04-14 16:31:07 -03:00
3d3e91d49d v12.44.9 2021-04-14 04:06:00 +03:00
f6e6d9ce8b Merge pull request #2251 from balena-io/1003-config-inject-umount
config inject/read/write: Fix umount errors with OS image files
2021-04-14 01:03:53 +00:00
0f9d78ab50 config inject/read/write: Fix umount errors with OS image files
Resolves: #1003
Change-type: patch
2021-04-13 23:30:19 +01:00
06f7683837 Refactor dependency import in utils/helpers.ts for performance
Change-type: patch
2021-04-13 22:14:13 +01:00
83a23d9f30 v12.44.8 2021-04-10 02:37:53 +03:00
ffa181a2c3 Merge pull request #2248 from balena-io/2185-fix-ndjson-parsing
push, logs: Fix parsing of local mode device logs (NDJSON stream)
2021-04-09 23:35:27 +00:00
d50d18d492 push, logs: Fix parsing of local mode device logs (NDJSON stream)
Resolves: #2185
Change-type: patch
2021-04-09 23:58:04 +01:00
0b0fb94834 v12.44.7 2021-04-09 22:00:07 +03:00
c1244c0c98 Merge pull request #2247 from balena-io/osConfigureFix
lib/commands/local/configure: Fix local configure when resin-wifi is …
2021-04-09 18:56:42 +00:00
213e54feb1 lib/commands/local/configure: Fix local configure when resin-wifi is not available on the image
Resolves: #2239
Change-type: patch
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
2021-04-09 21:20:47 +03:00
cc8a8513e9 v12.44.6 2021-04-07 22:44:44 +03:00
42c3236313 Merge pull request #2246 from balena-io/missing-arch-install-notes
Direct missing release installs to npm install method
2021-04-07 19:42:26 +00:00
91fd515266 Direct missing release installs to npm install method
Change-type: patch
Signed-off-by: Miguel Casqueira <miguel@balena.io>
2021-04-07 13:56:17 -04:00
57cd096612 v12.44.5 2021-04-07 19:59:18 +03:00
854501cf8d Merge pull request #2245 from balena-io/sdk-15.31.0
Update balena-sdk (15.31.0) and other dependencies
2021-04-07 16:56:55 +00:00
d44afa8c39 docs: Update install instructions re macOS installer notarization
Change-type: patch
2021-04-07 17:26:59 +01:00
b7500fc2c2 Update resin-compose-parse from 2.1.2 to 2.1.3
Change-type: patch
2021-04-07 17:26:43 +01:00
dc6c8d7472 Update balena-config-json from 4.1.0 to 4.1.1
Change-type: patch
2021-04-07 17:26:24 +01:00
5c5be8f7b7 Update etcher-sdk from 6.2.0 to 6.2.1
Change-type: patch
2021-04-07 17:14:13 +01:00
5bdd6c6034 Update balena-sdk from 15.29.0 to 15.31.0
Change-type: patch
2021-04-07 16:58:21 +01:00
a5bade99fc v12.44.4 2021-04-07 00:42:46 +03:00
9c3eb76856 Merge pull request #2186 from balena-io/notarization
Update macOS installer to avoid Apple's warning pop-up
2021-04-06 21:40:12 +00:00
973f1a9c40 Add notarization for macOS graphical installer
Change-type: patch
2021-04-06 16:56:07 -04:00
16ea0c9d6d v12.44.3 2021-04-05 01:47:08 +03:00
73bfe545e8 Merge pull request #2242 from balena-io/preload-docs
docs: Further clarify Docker requirements for preload
2021-04-04 22:45:24 +00:00
f53e658ca2 docs: Further clarify Docker requirements for preload
Change-type: patch
2021-04-04 23:02:48 +01:00
b66706e8ee v12.44.2 2021-04-02 20:25:58 +03:00
11e50466d5 Merge pull request #2240 from balena-io/remove-balenalib-images
docker: Remove balenalib images and docs
2021-04-02 17:23:50 +00:00
431c4b6e4a docker: Remove references to CLI docker images in the installation docs
Change-type: patch
2021-04-02 18:05:31 +01:00
d12490f816 docker: Remove balenalib images and docs
Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-04-02 07:57:52 -04:00
67b7b8b5d0 v12.44.1 2021-03-31 19:15:26 +03:00
16d1f0f06f Merge pull request #2235 from balena-io/docs-fix-broken-url
os/configure: Fix broken NetworkManager URL
2021-03-31 16:13:23 +00:00
9676ea94cb v12.44.0 2021-03-31 15:13:11 +03:00
df8ce0bbe0 Merge pull request #2168 from balena-io/node14fix
Make `os configure` and `local flash` work with Node 14
2021-03-31 12:11:13 +00:00
6437bb7511 os/configure: Fix broken NetworkManager URL
Update the broken NM URL to match the rest of the documentation.

Change-type: patch
Connects-to: balena-io/docs/#1757 balena-io/docs/#1522
Changelog-entry: os/configure: Fix broken NetworkManager URL
Signed-off-by: Mark Corbin <mark@balena.io>
2021-03-31 10:08:37 +00:00
ac96616e4e osConfigure/localFlash: Add support for Node.js v14
* Replace old resin-image-fs with newer balena-image-fs
* package.json: Remove resin-image-fs package
* package: Install dependencies that work with node14
* Remove resin-image-fs typings
* Fix etcher-sdk related types
* local/flash: Add unmountOnSuccess, write, direct properties on flash
	Taken from https://github.com/balena-io-modules/etcher-sdk/blob/master/examples/multi-destination.ts
* tests/utils/eol-conversion: Remove ext2fs sample binary
	Specifically ext2fs/build/Release/bindings.node
	I removed it because the file doesn't exist
* tests/test-data/pkg: Add new expected warnings darwin/linux/windows
* os/configure: Remove windows check
* local/flash: Check if environment is WSL and show warning message
* Get tests to pass with certain Node v14 warning messages
* INSTALL-WINDOWS: Remove os configure warning

Improve push and logs support for Node.js v14 (bump 'net-keepalive')

Resolves: #2200
Resolves: #1990
Change-type: minor
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
2021-03-31 01:15:47 +03:00
2737c9c53c v12.43.2 2021-03-26 17:31:50 +02:00
3b8a46f523 Merge pull request #2229 from balena-io/catch-dind-errors
docker: Improve handling of Docker-in-Docker errors
2021-03-26 15:29:46 +00:00
3ac1994941 v12.43.1 2021-03-26 01:15:19 +02:00
b3a6c6cb0f Merge pull request #2230 from balena-io/update-install-mac-docker
Improve installation docs regarding Docker Desktop version requirements
2021-03-25 23:12:20 +00:00
6d4faa7b2c Improve installation docs regarding Docker Desktop version requirements
Connects-to: #2228
Change-type: patch
2021-03-25 16:07:09 +00:00
9036ce9af3 docker: Improve handling of Docker-in-Docker errors
The `local` logging driver captures output from container’s stdout/stderr
and writes them to an internal storage that is optimized for performance and disk use.

We also want to capture these logs on startup to wait for success/failure.

Advise the use of `--privileged` when running Docker-in-Docker to avoid
various permissions issues encountered in testing.

Change-type: patch
Changlelog-entry: docker: Improve handling of Docker-in-Docker errors
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-03-25 14:02:22 +00:00
4911db640f v12.43.0 2021-03-23 03:01:51 +02:00
e7999f52a9 Merge pull request #2210 from balena-io/oclif-dev-cli-update
Add macOS uninstall script (sudo /usr/local/lib/balena-cli/bin/uninstall)
2021-03-23 00:59:30 +00:00
68b61e7424 Refactor automation scripts (reduce need for MSYS to build on Windows)
Change-type: patch
2021-03-23 00:04:43 +00:00
329b84d01e Add macOS uninstall script (sudo /usr/local/lib/balena-cli/bin/uninstall)
Change-type: minor
2021-03-23 00:04:43 +00:00
25b1dff5d8 Bump patch-package dependency and remove its own patch file
Change-type: patch
2021-03-22 16:12:44 +00:00
fb1768b4ca v12.42.2 2021-03-21 01:31:40 +02:00
cbc1e52256 Merge pull request #2227 from balena-io/2226-yaml-null-volume
push: Fix docker-compose.dev.yml serialization ("should be object,null" error)
2021-03-20 23:29:58 +00:00
37c2880996 push: Fix docker-compose.dev.yml serialization ("should be object,null" error)
Change-type: patch
2021-03-20 22:17:20 +00:00
835445be2e v12.42.1 2021-03-19 18:58:58 +02:00
52fe7481fc Merge pull request #2225 from balena-io/readme-bullet-spacing
Make README.md bullet point spacing uniform
2021-03-19 16:56:32 +00:00
88072173d0 Make README.md bullet point spacing uniform
Change-type: patch
Signed-off-by: Genadi Naydenov genadi@balena.com
2021-03-19 18:32:07 +02:00
fdc2bff063 v12.42.0 2021-03-19 15:28:52 +02:00
4f6f20f469 Merge pull request #2218 from chriswiggins/public-address
Public address
2021-03-19 13:26:48 +00:00
50af0760ce balena device: Display public IP address field
Change-type: minor
2021-03-19 14:41:21 +13:00
43906d22c8 Update balena-sdk from 15.20.0 to 15.29.0
Change-type: patch
2021-03-19 14:41:20 +13:00
43f1188f1d v12.41.3 2021-03-17 18:45:08 +02:00
2629a01c7f Merge pull request #2223 from balena-io/engines-npm-v7
Update supported npm version range in package.json (<7.0.0)
2021-03-17 16:42:55 +00:00
5fc009a6ae Update supported npm version range in package.json (<7.0.0)
Connects-to: #2221
Change-type: patch
2021-03-17 15:28:39 +00:00
480f84993b v12.41.2 2021-03-17 10:46:32 +02:00
d1fdbd927e Merge pull request #2208 from balena-io/linux-install-docs-sudo
Linux installation instructions: Add sudo configuration section
2021-03-17 08:44:22 +00:00
4bfd345b68 v12.41.1 2021-03-15 20:39:53 +02:00
d4a153d2ee Merge pull request #2212 from balena-io/klutchell/balenalib-dockerfiles
docker: Fix path to init when workdir is changed
2021-03-15 18:38:01 +00:00
3cff091e3a docker: Fix path to init when workdir is changed
Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-03-15 10:32:43 -04:00
b2ad9f1643 v12.41.0 2021-03-15 16:07:04 +02:00
f7623bef85 Merge pull request #2159 from balena-io/klutchell/balenalib-dockerfiles
dockerfiles: initial commit of balenalib dockerfiles
2021-03-15 14:04:52 +00:00
af63794571 docs: Add Docker to Advanced Installation instructions
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-03-15 08:34:34 -04:00
65d5bdff08 docker: Add Docker images with the CLI and Docker-in-Docker
Add Dockerfiles for alpine and debian images, based on
balenalib/arch-distro-node images.

Change-type: minor
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-03-15 08:34:23 -04:00
23165806aa v12.40.4 2021-03-09 19:36:10 +02:00
3649bafbb1 Merge pull request #2209 from balena-io/update-apple-certificate-name
macOS GUI installer: Update signing certificate name
2021-03-09 17:34:12 +00:00
c62445a399 macOS GUI installer: Update signing certificate name
Change-type: patch
2021-03-09 16:40:38 +00:00
b233ea3e3e Linux installation instructions: Add sudo configuration section
Change-type: patch
2021-03-07 23:31:31 +00:00
4fe660b3a5 v12.40.3 2021-03-06 18:51:02 +02:00
1f07cd1b1c Merge pull request #2207 from balena-io/fix-qemu-download-error-handling
build, deploy: Fix error handling when QEMU download fails
2021-03-06 16:48:59 +00:00
bcea5193a1 build, deploy: Fix error handling when QEMU download fails
Change-type: patch
2021-03-06 16:10:33 +00:00
8b99cd7170 v12.40.2 2021-02-24 00:45:01 +02:00
1986c9339c Merge pull request #2197 from balena-io/device-local-mode-markdown
docs: Fix missing markdown docs for device `deactivate` and `local-mode`
2021-02-23 22:43:00 +00:00
b90c9b0d7e docs: Fix missing markdown docs for device deactivate and local-mode
Change-type: patch
2021-02-23 22:10:52 +00:00
e28c3f9814 v12.40.1 2021-02-23 16:55:03 +02:00
d054ced541 Merge pull request #2194 from balena-io/klutchell/emulated-docs
docs: emphasize that push emulation is not required in most cases
2021-02-23 14:52:52 +00:00
c8e4d2c9a6 docs: emphasize that push emulation is not required in most cases
Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-02-23 08:55:25 -05:00
9671372b9e v12.40.0 2021-02-10 03:28:52 +02:00
2a4ff75203 Merge pull request #2177 from balena-io/livepush-compose-dev-overlay
Add support for docker-compose dev overlay
2021-02-10 01:26:52 +00:00
f3d750a024 Add support for docker-compose dev overlay in local pushes
Change-type: minor
Signed-off-by: Scott Lowe <scott@balena.io>
2021-02-09 13:06:03 +01:00
a701cd8d4d v12.39.1 2021-02-07 01:08:07 +02:00
e2c0c2f359 Merge pull request #2179 from balena-io/klutchell/qemu-v5.2.0+balena4
build/deploy: fix emulated builds to use fully static qemu binaries
2021-02-06 23:05:50 +00:00
15fc805f89 build/deploy: fix emulated builds to use fully static qemu binaries
Avoid possible situations where the local glibc may not support
the required syscalls for arm emulation during build/deploy.

Change-type: patch
Conneted-to: https://github.com/balena-io/qemu/issues/21
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-02-06 09:50:46 -05:00
0a995ecc49 v12.39.0 2021-02-04 19:41:10 +02:00
1ba992ada2 Merge pull request #2176 from balena-io/device-localmode
Add command `device local-mode`
2021-02-04 17:39:05 +00:00
e47fd0c887 Add command device local-mode
Change-type: minor
Resolves: #1304
Signed-off-by: Scott Lowe <scott@balena.io>
2021-02-04 15:36:32 +00:00
af1de34840 v12.38.10 2021-02-04 17:00:43 +02:00
96fb525378 Merge pull request #2169 from balena-io/deduplicated-msg
Improve build-time checks (automation/test-lock-deduplicated.sh)
2021-02-04 14:58:27 +00:00
3d1f16c0ab v12.38.9 2021-02-04 15:59:06 +02:00
6fb58a25fc Merge pull request #2175 from balena-io/cloud-build-orgs
Modify push to pass app slug to builder
2021-02-04 13:56:31 +00:00
e6b85c9cf8 Modify push to pass app slug to builder
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-02-04 10:41:00 +01:00
43b93e7fd4 v12.38.8 2021-01-29 17:04:38 +02:00
a05dcf08b8 Merge pull request #2173 from balena-io/klutchell/qemu-v5.2.0
build/deploy: Update QEMU to speed up emulated builds
2021-01-29 15:02:49 +00:00
9636985ee7 build/deploy: Update QEMU to speed up emulated builds
QEMU v5 has quite a few improvements over v4, and the speed
difference when emulating arm is quite noticible.

We tested this with, and without, our single-core limitation
patch and have not been able to reproduce the stability
issues we were seeing in v4 so the patch was removed in
this release.

Change-type: patch
Connects-to: https://github.com/balena-io/balena-io/issues/2340
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-01-29 09:26:34 -05:00
023fc57914 v12.38.7 2021-01-26 10:31:28 +02:00
492bdab2fe Merge pull request #2170 from balena-io/tunnel-help-openbalena
tunnel: Add note re openBalena version compatibility
2021-01-26 08:29:29 +00:00
941c365259 tunnel: Add note re openBalena version compatibility
Change-type: patch
2021-01-25 17:33:41 +00:00
fed58278c9 v12.38.6 2021-01-23 03:09:50 +02:00
d74af38bfe Merge pull request #2171 from balena-io/debug-logging
logging: note that the device supervisor version is operative
2021-01-23 01:07:30 +00:00
53926067ca logging: note that the device supervisor version is operative
Change-type: patch
Signed-off-by: Matthew McGinn <matthew@balena.io>
2021-01-22 16:53:15 -05:00
7181dc5401 v12.38.5 2021-01-22 12:59:32 +02:00
e35e13f9a7 Merge pull request #2163 from balena-io/switch-tunnel-to-tls
tls: Use TLS for tunnel connection
2021-01-22 10:57:39 +00:00
6e0638f3be Improve build-time checks (automation/test-lock-deduplicated.sh)
Change-type: patch
2021-01-21 21:29:10 +00:00
d60ec13d5c v12.38.4 2021-01-21 19:14:47 +02:00
731e50a757 Merge pull request #2166 from balena-io/engines-less-than-13
Update supported Node.js version range in package.json (<13.0.0)
2021-01-21 17:11:55 +00:00
b363d28664 Update supported Node.js version range in package.json (<13.0.0)
Change-type: patch
2021-01-21 15:58:08 +00:00
7ae83d9ce5 tls: Use TLS for tunnel connection
Switch to using the exposed tunnelUrl and TLS for making
tunnels to the device, to improve security.

Change-type: patch
Signed-off-by: Rich Bayliss <rich@balena.io>
2021-01-20 21:18:23 +00:00
31281549a6 v12.38.3 2021-01-19 19:19:10 +02:00
e86bcc438c Merge pull request #2161 from balena-io/workaround-push-public
Handle 'push' edge case with application access
2021-01-19 17:17:31 +00:00
a1cf602f6f Handle 'push' edge case with application access
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-01-19 13:38:22 +01:00
4cd3ef8b91 v12.38.2 2021-01-19 10:22:20 +02:00
e4eb4586f5 Merge pull request #2158 from balena-io/delete-travis-appveyor
Delete old config files for Travis and AppVeyor to avoid confusion
2021-01-19 08:20:37 +00:00
360c6e42f8 v12.38.1 2021-01-15 18:54:21 +02:00
f76702c4e0 Merge pull request #2160 from balena-io/fix-errorhandler-strings
Fix handling of thrown strings
2021-01-15 16:52:14 +00:00
d3586696b4 Fix handling of thrown strings
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-01-15 16:45:01 +01:00
f73e3db4de Delete old config files for Travis and AppVeyor to avoid confusion
Change-type: patch
2021-01-15 14:05:16 +00:00
1f74889386 v12.38.0 2021-01-15 01:27:04 +02:00
743de66138 Merge pull request #2154 from balena-io/add_release_tags_to_deploy
Add release-tag on deploy command
2021-01-14 23:24:51 +00:00
8d56fe9678 deploy: Add --release-tag flag
Now we can do:
`balena deploy myApp myApp/myImage --release-tag key1 value1`

Refactor and reuse the logic that parses and applies the
release tag options from the push command to the deploy
command.

Resolves: #892
Change-type: minor
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
2021-01-15 00:46:39 +02:00
3d9d8bf5c8 v12.37.2 2021-01-14 16:50:48 +02:00
8c3df9ae30 Merge pull request #2153 from balena-io/america
docs: americanize the spelling of words in sourced markdown
2021-01-14 14:48:52 +00:00
e71184ed3a docs: americanize the spelling of words in sourced markdown
Change-type: patch
Signed-off-by: Matthew McGinn <matthew@balena.io>
2021-01-13 10:12:24 -05:00
caadce6c2b v12.37.1 2021-01-06 17:30:57 +02:00
f45fac6138 Merge pull request #2148 from balena-io/remove-internal-scandevices
Refactor out command internal scandevices
2021-01-06 15:29:10 +00:00
aeff5997d0 Refactor out command internal scandevices
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-01-06 15:00:18 +01:00
b5028c65cc v12.37.0 2020-12-29 12:58:58 +02:00
f69276e7c9 Merge pull request #2146 from balena-io/update-preload-10.4.1
Update preload 10.4.1
2020-12-29 10:56:43 +00:00
9fff9266d4 Add --additional-space flag to preload
Change-type: minor
2020-12-28 17:08:20 +01:00
0e7f953f72 Update balena-preload to 10.4.1
10.4.0 improves image size estimation
10.4.1 prevents running out of space while pulling images because of temporary files

Change-type: patch
2020-12-28 16:42:12 +01:00
61b11994b5 v12.36.1 2020-12-24 02:20:29 +02:00
1e1935cfb1 Merge pull request #2144 from balena-io/orgs-push
Update push command for organizations
2020-12-24 00:18:38 +00:00
27e2b03702 Update push command for organizations
Change-type: patch
Connects-to: #2119
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-23 16:03:52 +01:00
358acbd2c8 v12.36.0 2020-12-23 09:36:42 +02:00
b040a21268 Merge pull request #2138 from balena-io/add_tag_on_push
push: Add --release-tag flag
2020-12-23 07:35:03 +00:00
074fe010bd errors: Make all exclusive flag errors expected
eg Don't report errors if during a push --release-tag
and --detached flags are used.

Change-type: minor
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
2020-12-22 17:10:10 +02:00
34557e35ee push: Add --release-tag flag
You can have 0 or multiple keys without values,
if you use values then you should have as many
values as you have keys. If you don't want to set
a value for a key set its value to "" (bash, cmd.exe)
or '""' (powershell).

Connects-to: #892
Change-type: minor
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
2020-12-22 17:10:10 +02:00
3bff569758 v12.35.3 2020-12-21 13:18:38 +02:00
cf06a8dfad Merge pull request #2143 from balena-io/improve-id-disambiguation-tags
Improve id disambiguation for tag commands
2020-12-21 11:16:55 +00:00
584aa745f7 Improve id disambiguation for tag commands
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-18 12:52:18 +01:00
194d12cb3d v12.35.2 2020-12-18 11:44:48 +02:00
7739379444 Merge pull request #2137 from balena-io/fix-balenadev-sigterm
Modify handling of SIGINT in balena-dev
2020-12-18 09:42:03 +00:00
5c93df921e Modify handling of SIGINT in balena-dev
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-18 10:19:55 +01:00
da652c6bce v12.35.1 2020-12-17 17:06:38 +02:00
1cd341e6cd Merge pull request #2139 from balena-io/org-support-ssh-tunnel
Update commands ssh, tunnel to support orgs
2020-12-17 15:04:34 +00:00
9d2884aab7 Update commands ssh, tunnel to support orgs
Change-type: patch
Connects-to: #2119
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-17 13:01:53 +01:00
f128eaf389 v12.35.0 2020-12-15 17:46:38 +02:00
70b0524eb6 Merge pull request #2131 from balena-io/update-app-command-info-for-orgs
Update various commands to support organizations
2020-12-15 15:44:12 +00:00
c898747468 Update various commands to support organizations
Change-type: minor
Connects-to: #2119
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-15 16:06:25 +01:00
6fc3b0df58 v12.34.0 2020-12-15 16:47:17 +02:00
746676beb9 Merge pull request #2127 from balena-io/app-create-orgs
Add organizations support to app create command
2020-12-15 14:42:38 +00:00
611f59a0da Add organizations support to app create command
Change-type: minor
Connects-to: #2119
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-15 14:58:17 +01:00
c6430274e5 v12.33.2 2020-12-15 09:40:26 +02:00
9637f75617 Merge pull request #2050 from josecoelho/1667-permission-validation
Improve error message to access balena settings
2020-12-15 07:37:57 +00:00
439d8391ee Improve error message for issues to access balena settings
Update balena-settings-storage from 6.0.1 to 7.0.0

Resolves: #1667
Change-type: patch
2020-12-15 20:14:54 +13:00
0d3ca63f00 v12.33.1 2020-12-11 18:44:05 +02:00
1f3677bdb2 Merge pull request #2136 from balena-io/fix-preload-app-id
Fix preload command support for application IDs
2020-12-11 16:41:03 +00:00
10bca728f0 v12.33.0 2020-12-11 17:12:40 +02:00
9763a14e97 Merge pull request #2135 from balena-io/add-orgs
Add orgs command
2020-12-11 15:10:13 +00:00
fe24280adf Fix preload command support for application IDs
Change-type: patch
Resolves: #2063
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-11 13:54:31 +00:00
a11f9ec705 Add orgs command
Change-type: minor
Connects-to: #2119
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-11 12:48:44 +01:00
836ae1cf4a v12.32.2 2020-12-11 03:11:16 +02:00
b4d37e7a3a Merge pull request #2133 from balena-io/apps-column-match-not-a-function
apps: Fix "column.match is not a function" when --verbose is used
2020-12-11 01:08:46 +00:00
055ad834e7 apps: Fix "column.match is not a function" when --verbose is used
Change-type: patch
2020-12-11 00:31:59 +00:00
d2cb88dfb8 v12.32.1 2020-12-11 02:30:10 +02:00
d096743e78 Merge pull request #2130 from balena-io/ab77/onprem-refresh
Make balena-cli build on refreshed on-prem workers
2020-12-11 00:26:45 +00:00
511d0dbe26 Make balena-cli build on refreshed on-prem workers
* Fix 'balena ssh' test cases when using the Windows built-in ssh tool
* Fix Windows installer build in new balena CI workers (qqjs patch)
* Remove hardcoded path

Change-type: patch
2020-12-10 12:30:25 -08:00
6b0201866f v12.32.0 2020-12-10 18:42:18 +02:00
9e20b2b691 Merge pull request #2128 from balena-io/app-rename-orgs
Add organizations support to app rename command
2020-12-10 16:40:18 +00:00
665e0cf9d7 Add organizations support to app rename command
Change-type: minor
Connects-to: #2119
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-10 13:57:42 +01:00
b319ec7281 v12.31.0 2020-12-10 14:38:47 +02:00
ae3ccf759f Merge pull request #2113 from balena-io/1828-livepush-connection-lost
Livepush, logs: Automatically reconnect on 'Connection to device lost'
2020-12-10 12:36:45 +00:00
309b1ba6a0 v12.30.4 2020-12-10 10:20:29 +02:00
532c4a1862 Merge pull request #2125 from balena-io/fix-app-display
Fix app name output in app command
2020-12-10 08:18:28 +00:00
fc8b7c71fc Fix app name output in app command
Change-type: patch
Resolves: #2120
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-10 08:32:20 +01:00
07666e953f Livepush: Extend CTRL-C availability (don't ignore CTRL-C during image build)
Change-type: patch
2020-12-09 22:49:47 +00:00
54731c2d20 Livepush, logs: Automatically reconnect on 'Connection to device lost'
Change-type: minor
2020-12-09 20:43:14 +00:00
d00db5ea8c logs: Fix CTRL-C ignored on Windows (PowerShell, MSYS, Git for Windows)
Change-type: patch
2020-12-09 20:43:14 +00:00
5497835728 Livepush: Fix process not exiting on "Connection to device lost"
Resolves: #1828
Change-type: patch
2020-12-09 20:43:14 +00:00
5bb05f3a8c v12.30.3 2020-12-09 19:07:19 +02:00
659eda8cd1 Merge pull request #2117 from balena-io/add_device_deactivation_expected_errors
errors: Add expected errors for device deactivation
2020-12-09 17:05:41 +00:00
a19132d3bf errors: Add expected errors for device deactivation
Change-type: patch
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
2020-12-09 14:52:51 +02:00
140993f554 v12.30.2 2020-12-08 13:02:07 +02:00
575eaf6de1 Merge pull request #2116 from balena-io/remove-v12
Remove remaining v12 switches
2020-12-08 11:00:07 +00:00
3edf7a038f Remove remaining v12 switches
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-08 11:00:36 +01:00
ad16c5270e v12.30.1 2020-12-07 16:14:13 +02:00
adadefdf3f Merge pull request #2115 from balena-io/fix-booleans
Standardize boolean flag typing
2020-12-07 14:12:14 +00:00
19fab40398 Standardize boolean flag typing
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2020-12-07 14:36:29 +01:00
4dc53eb056 v12.30.0 2020-12-07 14:30:09 +02:00
9c96da7515 Merge pull request #2112 from balena-io/add_deactive_cmd
device: Add deactivate command
2020-12-07 12:28:24 +00:00
8a3e386d21 packages: Bump balena-sdk and balena-errors
Update balena-sdk from 15.6.0 to 15.20.0
Update balena-errors from 4.4.1 to 4.7.1

Change-type: minor
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
2020-12-07 13:19:02 +02:00
5eaa4cfb9f common-flags: Add default false on yes, force and verbose flags
Change-type: patch
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
2020-12-07 13:17:51 +02:00
cb2b90732b device: Add deactivate command
Resolves: #1545
Change-type: minor
Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
2020-12-07 13:12:50 +02:00
090fc58d10 v12.29.1 2020-12-04 03:22:19 +02:00
3b05971098 Merge pull request #2114 from balena-io/devices-full-uuid
devices: Don't truncate device UUID to 7 chars when --json is used
2020-12-04 01:20:17 +00:00
aae6aff3e9 devices: Don't truncate device UUID to 7 chars when --json is used
Change-type: patch
2020-12-04 00:45:03 +00:00
120 changed files with 7702 additions and 3513 deletions

37
.dockerignore Normal file
View File

@ -0,0 +1,37 @@
# Reminders:
# * Matching rules are different to `.gitignore`
# * A pattern without '**' matches in the project's root directory only
# * Leading and trailing '/' are discarded (it is not possible to
# distinguish between files and directories)
# * More details: https://github.com/balena-io-modules/dockerignore
# development and testing tools or IDEs
**/*.log
**/*.pid
**/*.seed
.idea
.lock-wscript
.nvmrc
.nyc_output
.vscode
coverage
lib-cov
logs
pids
# OS cache files
**/.DS_Store
# balena CLI config and build files
**/.balenaconf
**/.fast-boot.json
**/.resinconf
balenarc.yml
build
build-bin
dist
node_modules
oclif.manifest.json
package-lock.json
resinrc.yml
tmp

69
.gitignore vendored
View File

@ -1,47 +1,36 @@
# Logs
logs
*.log
# Reminders:
# * A pattern without '/' matches in subdirectories as well (files and directories)
# * A leading '/' anchors matching to the directory where `.gitignore` is defined
# * A trailing '/' makes the pattern match against directories only
# More details: https://git-scm.com/docs/gitignore
# Runtime data
pids
# development and testing tools or IDEs
*.log
*.pid
*.seed
/.idea/
/.lock-wscript
/.nvmrc
/.nyc_output/
/.vscode/
/coverage/
/lib-cov/
/logs
/pids
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Commenting this out is preferred by some people, see
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
package-lock.json
.resinconf
.balenaconf
resinrc.yml
balenarc.yml
# OS cache files
.DS_Store
.idea
.nvmrc
.vscode
/tmp
build/
build-bin/
build-zip/
dist/
# Ignore fast-boot cache file
**/.fast-boot.json
# balena CLI config and build files
.balenaconf
.fast-boot.json
.resinconf
/balenarc.yml
/build/
/build-bin/
/dist/
/node_modules
/oclif.manifest.json
/package-lock.json
/resinrc.yml
/tmp/

View File

@ -15,6 +15,3 @@ npm:
- "10"
- "12"
- "14"
docker:
publish: false

View File

@ -1,25 +0,0 @@
language: node_js
os:
- linux
- osx
node_js:
- "10"
matrix:
exclude:
node_js: "10"
script:
- node --version
- npm --version
- npm run ci
# - npm run build:standalone
# - npm run build:installer
notifications:
email: false
deploy:
- provider: script
script: npm run release
skip_cleanup: true
on:
tags: true
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
repo: balena-io/balena-cli

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,451 @@ 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.44.11 - 2021-04-21
* Add message regarding deprecation of --buildArg option in build/deploy commands [Scott Lowe]
## 12.44.10 - 2021-04-15
* ssh: fix incorrect service name parsing in local mode [Tomás Migone]
## 12.44.9 - 2021-04-13
* config inject/read/write: Fix umount errors with OS image files [Paulo Castro]
* Refactor dependency import in utils/helpers.ts for performance [Paulo Castro]
## 12.44.8 - 2021-04-09
* push, logs: Fix parsing of local mode device logs (NDJSON stream) [Paulo Castro]
## 12.44.7 - 2021-04-09
* lib/commands/local/configure: Fix local configure when resin-wifi is not available on the image [Marios Balamatsias]
## 12.44.6 - 2021-04-07
* Direct missing release installs to npm install method [Miguel Casqueira]
## 12.44.5 - 2021-04-07
* docs: Update install instructions re macOS installer notarization [Paulo Castro]
* Update resin-compose-parse from 2.1.2 to 2.1.3 [Paulo Castro]
* Update balena-config-json from 4.1.0 to 4.1.1 [Paulo Castro]
* Update etcher-sdk from 6.2.0 to 6.2.1 [Paulo Castro]
* Update balena-sdk from 15.29.0 to 15.31.0 [Paulo Castro]
## 12.44.4 - 2021-04-06
* Add notarization for macOS graphical installer [Dan Goodman]
## 12.44.3 - 2021-04-04
* docs: Further clarify Docker requirements for preload [Paulo Castro]
## 12.44.2 - 2021-04-02
* docker: Remove references to CLI docker images in the installation docs [Paulo Castro]
* docker: Remove balenalib images and docs [Kyle Harding]
## 12.44.1 - 2021-03-31
* os/configure: Fix broken NetworkManager URL [Mark Corbin]
## 12.44.0 - 2021-03-30
* osConfigure/localFlash: Add support for Node.js v14 [Marios Balamatsias]
## 12.43.2 - 2021-03-26
* docker: Improve handling of Docker-in-Docker errors [Kyle Harding]
## 12.43.1 - 2021-03-25
* Improve installation docs regarding Docker Desktop version requirements [Paulo Castro]
## 12.43.0 - 2021-03-23
* Refactor automation scripts (reduce need for MSYS to build on Windows) [Paulo Castro]
* Add macOS uninstall script (sudo /usr/local/lib/balena-cli/bin/uninstall) [Paulo Castro]
* Bump `patch-package` dependency and remove its own patch file [Paulo Castro]
## 12.42.2 - 2021-03-20
* push: Fix docker-compose.dev.yml serialization ("should be object,null" error) [Paulo Castro]
## 12.42.1 - 2021-03-19
* Make README.md bullet point spacing uniform [Genadi Naydenov]
## 12.42.0 - 2021-03-19
* balena device: Display public IP address field [Chris Wiggins]
* Update balena-sdk from 15.20.0 to 15.29.0 [Chris Wiggins]
## 12.41.3 - 2021-03-17
* Update supported npm version range in package.json (<7.0.0) [Paulo Castro]
## 12.41.2 - 2021-03-17
* Linux installation instructions: Add sudo configuration section [Paulo Castro]
## 12.41.1 - 2021-03-15
* docker: Fix path to init when workdir is changed [Kyle Harding]
## 12.41.0 - 2021-03-15
* docker: Add Docker images with the CLI and Docker-in-Docker [Kyle Harding]
## 12.40.4 - 2021-03-09
* macOS GUI installer: Update signing certificate name [Paulo Castro]
## 12.40.3 - 2021-03-06
* build, deploy: Fix error handling when QEMU download fails [Paulo Castro]
## 12.40.2 - 2021-02-23
* docs: Fix missing markdown docs for device `deactivate` and `local-mode` [Paulo Castro]
## 12.40.1 - 2021-02-23
* docs: emphasize that push emulation is not required in most cases [Kyle Harding]
## 12.40.0 - 2021-02-09
* Add support for docker-compose dev overlay in local pushes [Scott Lowe]
## 12.39.1 - 2021-02-06
* build/deploy: fix emulated builds to use fully static qemu binaries [Kyle Harding]
## 12.39.0 - 2021-02-04
* Add command `device local-mode` [Scott Lowe]
## 12.38.10 - 2021-02-04
* Improve build-time checks (automation/test-lock-deduplicated.sh) [Paulo Castro]
## 12.38.9 - 2021-02-04
* Modify push to pass app slug to builder [Scott Lowe]
## 12.38.8 - 2021-01-29
* build/deploy: Update QEMU to speed up emulated builds [Kyle Harding]
## 12.38.7 - 2021-01-25
* tunnel: Add note re openBalena version compatibility [Paulo Castro]
## 12.38.6 - 2021-01-22
* logging: note that the device supervisor version is operative [Matthew McGinn]
## 12.38.5 - 2021-01-21
* tls: Use TLS for tunnel connection [Balena CI]
## 12.38.4 - 2021-01-21
* Update supported Node.js version range in package.json (<13.0.0) [Paulo Castro]
## 12.38.3 - 2021-01-19
* Handle 'push' edge case with application access [Scott Lowe]
## 12.38.2 - 2021-01-18
* Delete old config files for Travis and AppVeyor to avoid confusion [Paulo Castro]
## 12.38.1 - 2021-01-15
* Fix handling of thrown strings [Scott Lowe]
## 12.38.0 - 2021-01-14
* deploy: Add --release-tag flag [Marios Balamatsias]
## 12.37.2 - 2021-01-13
* docs: americanize the spelling of words in sourced markdown [Matthew McGinn]
## 12.37.1 - 2021-01-06
* Refactor out command internal scandevices [Scott Lowe]
## 12.37.0 - 2020-12-28
* Add --additional-space flag to preload [Alexis Svinartchouk]
* Update balena-preload to 10.4.1 [Alexis Svinartchouk]
## 12.36.1 - 2020-12-23
* Update push command for organizations [Scott Lowe]
## 12.36.0 - 2020-12-22
* errors: Make all exclusive flag errors expected [Marios Balamatsias]
* push: Add --release-tag flag [Marios Balamatsias]
## 12.35.3 - 2020-12-21
* Improve id disambiguation for tag commands [Scott Lowe]
## 12.35.2 - 2020-12-18
* Modify handling of SIGINT in balena-dev [Scott Lowe]
## 12.35.1 - 2020-12-17
* Update commands ssh, tunnel to support orgs [Scott Lowe]
## 12.35.0 - 2020-12-15
* Update various commands to support organizations [Scott Lowe]
## 12.34.0 - 2020-12-15
* Add organizations support to app create command [Scott Lowe]
## 12.33.2 - 2020-12-15
* Improve error message for issues to access balena settings [josecoelho]
## 12.33.1 - 2020-12-11
* Fix preload command support for application IDs [Scott Lowe]
## 12.33.0 - 2020-12-11
* Add orgs command [Scott Lowe]
## 12.32.2 - 2020-12-11
* apps: Fix "column.match is not a function" when --verbose is used [Paulo Castro]
## 12.32.1 - 2020-12-10
* Make balena-cli build on refreshed on-prem workers [Paulo Castro]
## 12.32.0 - 2020-12-10
* Add organizations support to app rename command [Scott Lowe]
## 12.31.0 - 2020-12-10
* Livepush: Extend CTRL-C availability (don't ignore CTRL-C during image build) [Paulo Castro]
* Livepush, logs: Automatically reconnect on 'Connection to device lost' [Paulo Castro]
* logs: Fix CTRL-C ignored on Windows (PowerShell, MSYS, Git for Windows) [Paulo Castro]
* Livepush: Fix process not exiting on "Connection to device lost" [Paulo Castro]
## 12.30.4 - 2020-12-10
* Fix app name output in app command [Scott Lowe]
## 12.30.3 - 2020-12-09
* errors: Add expected errors for device deactivation [Marios Balamatsias]
## 12.30.2 - 2020-12-08
* Remove remaining v12 switches [Scott Lowe]
## 12.30.1 - 2020-12-07
* Standardize boolean flag typing [Scott Lowe]
## 12.30.0 - 2020-12-07
<details>
<summary> packages: Bump balena-sdk and balena-errors [Marios Balamatsias] </summary>
> ### balena-sdk-15.20.0 - 2020-12-04
>
> * device: Add deactivate method [Marios Balamatsias]
>
> ### balena-sdk-15.19.0 - 2020-12-02
>
> * Add missing application and release typings [Stevche Radevski]
>
> ### balena-sdk-15.18.1 - 2020-11-20
>
> * Bump typescript to 4.1 [Thodoris Greasidis]
>
> ### balena-sdk-15.18.0 - 2020-11-19
>
> * typings: Deprecate PineWithSelectOnGet variant in favor of PineStrict [Thodoris Greasidis]
>
> <details>
> <summary> Update balena-auth from 4.0.2 to 4.1.0 [josecoelho] </summary>
>
>> #### balena-request-11.2.0 - 2020-11-12
>>
>> * Update balena-auth from 4.0.0 to 4.1.0 [josecoelho]
>>
> </details>
>
>
> ### balena-sdk-15.17.0 - 2020-10-27
>
> * Add missing reverse navigation relations to User typings [Thodoris Greasidis]
>
> ### balena-sdk-15.16.0 - 2020-10-23
>
> * Add SDK methods for org invites [Amit Solanki]
>
> ### balena-sdk-15.15.0 - 2020-10-22
>
> * Modify the os update to check against hostapp release [Stevche Radevski]
>
> ### balena-sdk-15.14.0 - 2020-10-19
>
> * Prevent invalid $selects in strict pine.get variant calls [Thodoris Greasidis]
> * Improve the parameter type checks for the fully typed pine.get [Thodoris Greasidis]
>
> ### balena-sdk-15.13.0 - 2020-10-09
>
> * Pass shouldFlatten through when creating release from url [Stevche Radevski]
>
> ### balena-sdk-15.12.1 - 2020-09-20
>
> * Time the test suites [Thodoris Greasidis]
> * Combine test util files from before dropping coffeescript [Thodoris Greasidis]
>
> ### balena-sdk-15.12.0 - 2020-09-20
>
> * Application: add rename method [JSReds]
>
> ### balena-sdk-15.11.3 - 2020-09-19
>
> * tests/keys: Fix race condition [Thodoris Greasidis]
>
> ### balena-sdk-15.11.2 - 2020-09-19
>
> * tests/device: Combine some multicontainer app tests [Thodoris Greasidis]
> * Remove some beforeEach() from the device tests [Thodoris Greasidis]
>
> ### balena-sdk-15.11.1 - 2020-09-19
>
> * Fix the device.setSupervisorRelease() tests [Thodoris Greasidis]
>
> ### balena-sdk-15.11.0 - 2020-09-14
>
> * Typings: Extend the supported billing cycles [Thodoris Greasidis]
>
> ### balena-sdk-15.10.6 - 2020-09-14
>
> * tests: Reduce the application creations & teardowns even further [Thodoris Greasidis]
>
> ### balena-sdk-15.10.5 - 2020-09-14
>
> * Login: add new error handling, update balena-errors [JSReds]
>
> ### balena-sdk-15.10.4 - 2020-09-11
>
> * tests: Reduce the application creations & teardowns [Thodoris Greasidis]
>
> ### balena-sdk-15.10.3 - 2020-09-11
>
> * tests: Use mocha.parallel to speed up the test cases [Thodoris Greasidis]
>
> ### balena-sdk-15.10.2 - 2020-09-11
>
> * tests: Remove some before/afterEach calls to speed up the tests [Thodoris Greasidis]
>
> ### balena-sdk-15.10.1 - 2020-09-10
>
> * tests: Test that the result of device.getDeviceSlug() is a string [Thodoris Greasidis]
> * tests: Run device.getDeviceBySlug() calls in parallel to speed up tests [Thodoris Greasidis]
> * tests/os: Drop unnecessary beforeEach in getConfig() [Thodoris Greasidis]
> * tests/application: Fix incorrect skipping of unauthenticated tests [Thodoris Greasidis]
>
> ### balena-sdk-15.10.0 - 2020-09-10
>
> * typings: Make ReleaseWithImageDetails more accurate [Thodoris Greasidis]
> * Fully type the pine.get results [Thodoris Greasidis]
> * typings: Add the PineTypedResult helper type [Thodoris Greasidis]
>
> ### balena-sdk-15.9.1 - 2020-09-09
>
> * Typings: Add organization member relation to tags [Thodoris Greasidis]
>
> ### balena-sdk-15.9.0 - 2020-09-08
>
> * Add typings for pine.getOrCreate() [Thodoris Greasidis]
>
> <details>
> <summary> Bump balena-pine to add getOrCreate [Thodoris Greasidis] </summary>
>
>> #### balena-pine-12.4.0 - 2020-09-07
>>
>>
>> <details>
>> <summary> Update pinejs-client-core to 6.9.0 to support getOrCreate() [Thodoris Greasidis] </summary>
>>
>>> ##### pinejs-client-js-6.9.0 - 2020-09-07
>>>
>>> * Add 'getOrCreate' method supporting natural keys [Thodoris Greasidis]
>>>
>>> ##### pinejs-client-js-6.8.0 - 2020-09-03
>>>
>>> * Add support for $format [Pagan Gazzard]
>>>
>>> ##### pinejs-client-js-6.7.3 - 2020-08-26
>>>
>>> * Improve $orderby typing to allow `[{a: 'desc'}, {b: 'asc'}]` [Pagan Gazzard]
>>>
>>> ##### pinejs-client-js-6.7.2 - 2020-08-24
>>>
>>> * Update dev dependencies [Pagan Gazzard]
>>>
>>> ##### pinejs-client-js-6.7.1 - 2020-08-12
>>>
>>> * Fix prepare $count typings [Pagan Gazzard]
>>>
>>> ##### pinejs-client-js-6.7.0 - 2020-08-12
>>>
>>> * Improve typings for request/post/put/patch/delete [Pagan Gazzard]
>>>
>> </details>
>>
>>
> </details>
>
>
> ### balena-sdk-15.8.1 - 2020-09-08
>
> * Add mocha tests specific linting [Thodoris Greasidis]
> * Auto-fix lint errors with the test:fast script [Thodoris Greasidis]
> * Add linting checks back to the test script [Thodoris Greasidis]
>
> ### balena-sdk-15.8.0 - 2020-09-08
>
> * Add a hostapps model for fetching OS versions [Stevche Radevski]
>
> ### balena-sdk-15.7.1 - 2020-09-03
>
> * tests: Convert the device.getMACAddress tests to async await [Thodoris Greasidis]
>
> ### balena-sdk-15.7.0 - 2020-09-03
>
> * Add methods for managing organization membership tags [Thodoris Greasidis]
> * tests: Support testing tags with two word names [Thodoris Greasidis]
>
</details>
* common-flags: Add default false on yes, force and verbose flags [Marios Balamatsias]
* device: Add deactivate command [Marios Balamatsias]
## 12.29.1 - 2020-12-04
* devices: Don't truncate device UUID to 7 chars when --json is used [Paulo Castro]
## 12.29.0 - 2020-12-01
* scan: Print production devices' info on scan [Marios Balamatsias]

View File

@ -127,11 +127,15 @@ The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
## Windows
The `npm run build:installer` script (which generates the `.exe` executable installer on Windows)
specifically requires [MSYS2](https://www.msys2.org/) to be installed. Other than that, the
standard Command Prompt or PowerShell can be used (though MSYS2 is still handy, as it provides
'git' and a number of common unix utilities). If changes are made to npm scripts in `package.json`,
check that they also run on a standard Windows Command Prompt.
Besides the regular npm installation dependencies, the `npm run build:installer` script
that produces the `.exe` graphical installer on Windows also requires
[NSIS](https://sourceforge.net/projects/nsis/) and [MSYS2](https://www.msys2.org/) to be
installed. Be sure to add `C:\Program Files (x86)\NSIS` to the PATH, so that `makensis`
is available. MSYS2 is recommended when developing the balena CLI on Windows.
If changes are made to npm scripts in `package.json`, don't assume that a Unix shell like
bash is available. For example, some Windows shells don't have the `cp` and `rm` commands,
which is why you'll often find `ncp` and `rimraf` used in `package.json` scripts.
## Updating the 'npm-shrinkwrap.json' file

View File

@ -60,7 +60,7 @@ macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
[Windows](https://www.computerhope.com/issues/ch000549.htm)
> * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and
> workaround](https://github.com/balena-io/balena-cli/issues/1479).
> workaround](https://github.com/balena-io/balena-cli/issues/2244).
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
> For these, consider the [NPM Installation](#npm-installation) option.
@ -83,9 +83,7 @@ some additional development tools to be installed first:
Installing Node via [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) is recommended.
When the "system" or "default" Node.js and npm packages are installed with "apt-get" in Linux
distributions like Ubuntu, users often report permission or compilation errors when running
"npm install". This [sample
Dockerfile](https://gist.github.com/pdcastro/5d4d96652181e7da685a32caf629dd44) shows the CLI
installation steps on an Ubuntu 18.04 base image.
"npm install".
* [Python 2.7](https://www.python.org/), [git](https://git-scm.com/), [make](https://www.gnu.org/software/make/), [g++](https://gcc.gnu.org/)
* **Linux** and **Windows Subsystem for Linux (WSL):**
`sudo apt-get install -y python git make g++`
@ -95,9 +93,8 @@ some additional development tools to be installed first:
On **Windows (not WSL),** the dependencies above and additional ones can be met by installing:
* Node.js from the [Nodejs.org download page](https://nodejs.org/en/download/).
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++`, `ssh`, `rsync`
and more:
* `pacman -S git openssh rsync gcc make`
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++`, `7z` and more:
* `pacman -S git gcc make openssh p7zip`
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based
interactive CLI menus to misbehave. [Check this Github issue for a
@ -127,7 +124,7 @@ regular (non-root) user account, especially if using a user-managed node install
## Additional Dependencies
The `balena ssh`, `scan`, `build`, `deploy`, `preload` and `os configure` commands may require
The `balena ssh`, `scan`, `build`, `deploy` and `preload` commands may require
additional software to be installed. Check the Additional Dependencies sections for each operating
system:
@ -135,8 +132,8 @@ system:
* [macOS](./INSTALL-MAC.md#additional-dependencies)
* [Linux](./INSTALL-LINUX.md#additional-dependencies)
The `build` and `deploy` commands are also capable of using Docker or balenaEngine on a remote
server, or on a balenaOS device running a [balenaOS development
Where Docker or balenaEngine are required, they may be installed on the local machine (where the
balena CLI is executed), on a remote server, or on a balenaOS device running a [balenaOS development
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images)). Reasons why this
may be desirable include:
@ -145,6 +142,7 @@ may be desirable include:
* To build or run images "natively" on an ARM device, avoiding the need for QEMU emulation.
To use a remote Docker Engine (daemon) or balenaEngine, specify the remote machine's IP address and
port number with the `--dockerHost` and `--dockerPort` command-line options. For more details,
check `balena help build` or the [online
port number with the `--dockerHost` and `--dockerPort` command-line options. The `preload` command
has additional requirements because the bind mount feature is used. For more details, see
`balena help` for each command or the [online
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).

View File

@ -1,8 +1,6 @@
# balena CLI Installation Instructions for Linux
These instructions are for the recommended installation option. They are suitable for most Linux
distributions, except notably for **Linux Alpine** or **Busybox**. For these distros, see [advanced
installation options](./INSTALL-ADVANCED.md).
These instructions are suitable for most Linux distributions on Intel x86, except notably for **Linux Alpine** or **Busybox**. For these distros or for the ARM architecture, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation) method.
Selected operating system: **Linux**
@ -11,36 +9,53 @@ Selected operating system: **Linux**
with "-standalone.zip", for example:
`balena-cli-vX.Y.Z-linux-x64-standalone.zip`
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
`balena-cli` folder.
2. Extract the zip file contents to any folder you choose, for example `/home/james`.
The extracted contents will include a `balena-cli` folder.
3. Add the `balena-cli` folder to the system's `PATH` environment variable. There are several
ways of achieving this on Linux: See this [StackOverflow post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix). Close and reopen the terminal window
so that the changes to PATH can take effect.
3. Add that folder (e.g. `/home/james/balena-cli`) to the `PATH` environment variable.
Check this [StackOverflow
post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)
for instructions. Close and reopen the terminal window so that the changes to `PATH`
can take effect.
4. Check that the installation was successful by running the following commands on a
command terminal:
terminal window:
* `balena version` - should print the CLI's version
* `balena help` - should print a list of available commands
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
`deploy` and `preload` commands may require additional software to be installed, as described
below.
To update the balena CLI to a new version, download a new release zip file and replace the previous
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
as described above.
## sudo configuration
A few CLI commands require execution through sudo, e.g. `sudo balena scan`.
If your Linux distribution has an `/etc/sudoers` file that defines a `secure_path`
setting, run `sudo visudo` to edit it and add the balena CLI's installation folder to
the ***pre-existing*** `secure_path` setting, for example:
```text
Defaults secure_path="/home/james/balena-cli:<pre-existing entries go here>"
```
If an `/etc/sudoers` file does not exist, or if it does not contain a pre-existing
`secure_path` setting, do not change it.
If you also have Docker installed, ensure that it can be executed ***without*** `sudo`, so that
CLI commands like `balena build` and `balena preload` can also be executed without `sudo`.
Check Docker's [post-installation
steps](https://docs.docker.com/engine/install/linux-postinstall/) on how to achieve this.
## Additional Dependencies
### build, deploy
These commands require [Docker](https://docs.docker.com/install/overview/) or
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote machine). Most
users will simply follow [Docker's installation
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
machine) where the balena CLI is installed. The [advanced installation
options](./INSTALL-ADVANCED.md) document describes other possibilities.
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
machine. Most users will follow [Docker's installation
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
workstation as the balena CLI. The [advanced installation
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
### balena ssh

View File

@ -22,18 +22,26 @@ Selected operating system: **macOS**
* `balena help` - should print a list of available commands
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
and `preload` commands may require additional software to be installed, as described below.
and `preload` commands may require additional software to be installed, as described
in the next section.
To update the balena CLI, repeat the steps above for the new version.
To uninstall it, run the following command on a terminal prompt:
```text
sudo /usr/local/lib/balena-cli/bin/uninstall
```
## Additional Dependencies
### build and deploy
These commands require [Docker](https://docs.docker.com/install/overview/) or
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote machine). Most
users will simply follow [Docker's installation
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
machine) where the balena CLI is installed. The [advanced installation
options](./INSTALL-ADVANCED.md) document describes other possibilities.
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
machine. Most users will follow [Docker's installation
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
workstation as the balena CLI. The [advanced installation
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
### balena ssh
@ -52,17 +60,17 @@ command set can also be used to list and manage SSH keys: see `balena help -v`.
### balena preload
Like the `build` and `deploy` commands, the `preload` command requires Docker, with the additional
restriction that Docker must be installed on the local machine (because Docker's bind mounting
feature is used). Also, for some device types (such as the Raspberry Pi), the `preload` command
requires Docker to support the [AUFS storage
Like the `build` and `deploy` commands, the `preload` command requires Docker.
Preloading balenaOS images for some older device types (like the Raspberry
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
for Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1. The
present workaround is to either:
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
18.06.1. The present workarounds are to either:
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
Mac](https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18061-ce-mac73-2018-08-29)
* Install the balena CLI on a Linux machine (as Docker for Linux still supports AUFS). A Linux
Virtual Machine also works, but a Docker container is _not_ recommended.
Long term, we are working on replacing AUFS with overlay2 for the affected device types.
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.

View File

@ -22,7 +22,7 @@ Selected operating system: **Windows**
* `balena help` - should print a list of available commands
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
`deploy`, `preload` and `os configure` commands may require additional software to be installed, as
`deploy` and `preload` commands may require additional software to be installed, as
described below.
## Additional Dependencies
@ -30,11 +30,11 @@ described below.
### build and deploy
These commands require [Docker](https://docs.docker.com/install/overview/) or
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote machine). Most
users will simply follow [Docker's installation
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
machine) where the balena CLI is installed. The [advanced installation
options](./INSTALL-ADVANCED.md) document describes other possibilities.
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
machine. Most users will follow [Docker's installation
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
workstation as the balena CLI. The [advanced installation
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
### balena ssh
@ -59,24 +59,17 @@ Otherwise, Bonjour for Windows can be downloaded and installed from: https://sup
### balena preload
Like the `build` and `deploy` commands, the `preload` command requires Docker, with the additional
restriction that Docker must be installed on the local machine (because Docker's bind mounting
feature is used). Also, for some device types (such as the Raspberry Pi), the `preload` command
requires Docker to support the [AUFS storage
Like the `build` and `deploy` commands, the `preload` command requires Docker.
Preloading balenaOS images for some older device types (like the Raspberry
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
for Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1. The
present workaround is to either:
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
18.06.1. The present workarounds are to either:
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
Windows](https://docs.docker.com/docker-for-windows/release-notes/#docker-community-edition-18061-ce-win73-2018-08-29)
* Install the balena CLI on a Linux machine (as Docker for Linux still supports AUFS). A Linux
Virtual Machine also works, but a Docker container is _not_ recommended.
Long term, we are working on replacing AUFS with overlay2 for the affected device types.
### balena os configure
* The `balena os configure` command is currently not supported on Windows natively, but works with
the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL). When
using WSL, [install the balena CLI for
Linux](https://github.com/balena-io/balena-cli/blob/master/INSTALL-LINUX.md).
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.

View File

@ -28,18 +28,20 @@ are supported. Alternative shells include:
* [MSYS2](https://www.msys2.org/):
* Install additional packages with the command:
`pacman -S git openssh rsync`
`pacman -S git gcc make openssh p7zip`
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based interactive CLI
menus to break. [Check this Github issue for a
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
* [MSYS](http://www.mingw.org/wiki/MSYS): select the `msys-rsync` and `msys-openssh` packages too
* [MSYS](http://www.mingw.org/wiki/MSYS)
* [Git for Windows](https://git-for-windows.github.io/)
* During the installation, you will be prompted to choose between _"Use MinTTY"_ and _"Use
Windows' default console window"._ Choose the latter, because of the same [MSYS2
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** should be selected. See
@ -75,7 +77,6 @@ HTTP(S) proxies can be configured through any of the following methods, in prece
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'

View File

@ -1,43 +0,0 @@
# appveyor file
# http://www.appveyor.com/docs/appveyor-yml
image: Visual Studio 2017
init:
- git config --global core.autocrlf input
cache:
- C:\Users\appveyor\.node-gyp
- '%AppData%\npm-cache'
matrix:
fast_finish: true
# what combinations to test
environment:
matrix:
- nodejs_version: 10
install:
- ps: Install-Product node $env:nodejs_version x64
- set PATH=%APPDATA%\npm;%PATH%
- npm config set python 'C:\Python27\python.exe'
- npm --version
# - npm install
build: off
test: off
deploy: off
test_script:
- node --version
- npm --version
# - npm test
deploy_script:
- node --version
- npm --version
# - npm run build:standalone
# - npm run build:installer
# - IF "%APPVEYOR_REPO_TAG%" == "true" (npm run release)
# - IF NOT "%APPVEYOR_REPO_TAG%" == "true" (echo 'Not tagged, skipping deploy')

View File

@ -0,0 +1,5 @@
FROM ubuntu:20.04
WORKDIR /usr/src/app
RUN apt-get -q update && apt-get -qy install fuse file curl unzip
COPY balena.desktop icon.png make-appimage.sh ./
RUN chmod +x /usr/src/app/make-appimage.sh

View File

@ -0,0 +1,7 @@
[Desktop Entry]
Name=balena
Exec=balena
Icon=icon
Type=Application
Terminal=true
Categories=Utility;

BIN
automation/appimage/icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
curl -LO https://github.com/balena-io/balena-cli/releases/download/v12.44.11/balena-cli-v12.44.11-linux-x64-standalone.zip
curl -Lo /usr/local/bin/appimagetool https://github.com/AppImage/AppImageKit/releases/download/12/appimagetool-x86_64.AppImage
chmod +x /usr/local/bin/appimagetool
unzip -q balena-cli-v12.44.11-linux-x64-standalone.zip
mv balena-cli/balena balena-cli/AppRun
cp balena.desktop icon.png balena-cli/
ARCH=x86_64 appimagetool balena-cli
mv balena-x86_64.AppImage /usr/local/bin/balena
/usr/local/bin/balena version -a

View File

@ -28,13 +28,14 @@ import * as path from 'path';
import * as rimraf from 'rimraf';
import * as semver from 'semver';
import * as util from 'util';
import * as klaw from 'klaw';
import { Stats } from 'fs';
import { stripIndent } from '../lib/utils/lazy';
import {
diffLines,
getSubprocessStdout,
loadPackageJson,
MSYS2_BASH,
ROOT,
StdOutTap,
whichSpawn,
@ -43,6 +44,8 @@ import {
export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version;
const arch = process.arch;
const MSYS2_BASH =
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
function dPath(...paths: string[]) {
return path.join(ROOT, 'dist', ...paths);
@ -296,6 +299,88 @@ async function zipPkg() {
});
}
async function signFilesForNotarization() {
console.log('Deleting unneeded zip files...');
await new Promise((resolve, reject) => {
klaw('node_modules/')
.on('data', (item: { path: string; stats: Stats }) => {
if (!item.stats.isFile()) {
return;
}
if (
path.basename(item.path).endsWith('.zip') &&
path.dirname(item.path).includes('test')
) {
console.log('Removing zip', item.path);
fs.unlinkSync(item.path);
}
})
.on('end', resolve)
.on('error', reject);
});
// Sign all .node files first
console.log('Signing .node files...');
await new Promise((resolve, reject) => {
klaw('node_modules/')
.on('data', async (item: { path: string; stats: Stats }) => {
if (!item.stats.isFile()) {
return;
}
if (path.basename(item.path).endsWith('.node')) {
console.log('running command:', 'codesign', [
'-d',
'-f',
'-s',
'Developer ID Application: Balena Ltd (66H43P8FRG)',
item.path,
]);
await whichSpawn('codesign', [
'-d',
'-f',
'-s',
'Developer ID Application: Balena Ltd (66H43P8FRG)',
item.path,
]);
}
})
.on('end', resolve)
.on('error', reject);
});
console.log('Signing other binaries...');
console.log('running command:', 'codesign', [
'-d',
'-f',
'--options=runtime',
'-s',
'Developer ID Application: Balena Ltd (66H43P8FRG)',
'node_modules/denymount/bin/denymount',
]);
await whichSpawn('codesign', [
'-d',
'-f',
'--options=runtime',
'-s',
'Developer ID Application: Balena Ltd (66H43P8FRG)',
'node_modules/denymount/bin/denymount',
]);
console.log('running command:', 'codesign', [
'-d',
'-f',
'--options=runtime',
'-s',
'Developer ID Application: Balena Ltd (66H43P8FRG)',
'node_modules/macmount/bin/macmount',
]);
await whichSpawn('codesign', [
'-d',
'-f',
'--options=runtime',
'-s',
'Developer ID Application: Balena Ltd (66H43P8FRG)',
'node_modules/macmount/bin/macmount',
]);
}
export async function buildStandaloneZip() {
console.log(`Building standalone zip package for CLI ${version}`);
try {
@ -343,6 +428,20 @@ async function signWindowsInstaller() {
}
}
/**
* Wait for Apple Installer Notarization to continue
*/
async function notarizeMacInstaller(): Promise<void> {
const appleId = 'accounts+apple@balena.io';
const { notarize } = await import('electron-notarize');
await notarize({
appBundleId: 'io.balena.etcher',
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword: '@keychain:CLI_PASSWORD',
});
}
/**
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
* of process.platform) to generate the native installers (which end up under
@ -369,6 +468,10 @@ export async function buildOclifInstaller() {
console.log(`rimraf(${dir})`);
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
}
if (process.platform === 'darwin') {
console.log('Signing files for notarization...');
await signFilesForNotarization();
}
console.log('=======================================================');
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
@ -381,6 +484,10 @@ export async function buildOclifInstaller() {
// (`oclif.macos.sign` section).
if (process.platform === 'win32') {
await signWindowsInstaller();
} else if (process.platform === 'darwin') {
console.log('Notarizing package...');
await notarizeMacInstaller(); // Notarize
console.log('Package notarized.');
}
console.log(`oclif installer build completed`);
}

View File

@ -59,8 +59,10 @@ const capitanoDoc = {
'build/commands/devices/index.js',
'build/commands/devices/supported.js',
'build/commands/device/index.js',
'build/commands/device/deactivate.js',
'build/commands/device/identify.js',
'build/commands/device/init.js',
'build/commands/device/local-mode.js',
'build/commands/device/move.js',
'build/commands/device/os-update.js',
'build/commands/device/public-url.js',

View File

@ -27,7 +27,6 @@ import {
release,
updateDescriptionOfReleasesAffectedByIssue1359,
} from './deploy-bin';
import { fixPathForMsys, ROOT, runUnderMsys } from './utils';
// DEBUG set to falsy for negative values else is truthy
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
@ -48,9 +47,6 @@ function exitWithError(error: Error | string): never {
* 'build:standalone' (to build a standalone pkg package)
* 'release' (to create/update a GitHub release)
*
* In the case of 'build:installer', also call runUnderMsys() to switch the
* shell from cmd.exe to MSYS2 bash.exe.
*
* @param args Arguments to parse (default is process.argv.slice(2))
*/
export async function run(args?: string[]) {
@ -74,10 +70,6 @@ export async function run(args?: string[]) {
}
}
// If runUnderMsys() is called to re-execute this script under MSYS2,
// the current working dir becomes the MSYS2 homedir, so we change back.
process.chdir(ROOT);
// The BUILD_TMP env var is used as an alternative location for oclif
// (patched) to copy/extract the CLI files, run npm install and then
// create the NSIS executable installer for Windows. This was necessary
@ -95,23 +87,6 @@ export async function run(args?: string[]) {
for (const arg of args) {
try {
if (arg === 'build:installer' && process.platform === 'win32') {
// ensure running under MSYS2
if (!process.env.MSYSTEM) {
process.env.MSYS2_PATH_TYPE = 'inherit';
await runUnderMsys([
fixPathForMsys(process.argv[0]),
fixPathForMsys(process.argv[1]),
arg,
]);
continue;
}
if (process.env.MSYS2_PATH_TYPE !== 'inherit') {
throw new Error(
'the MSYS2_PATH_TYPE env var must be set to "inherit"',
);
}
}
const cmdFunc = commands[arg];
await cmdFunc();
} catch (err) {

View File

@ -10,7 +10,10 @@ npm i
if ! diff -q npm-shrinkwrap.json npm-shrinkwrap.json.old > /dev/null; then
rm npm-shrinkwrap.json.old
echo "** npm-shrinkwrap.json was not deduplicated or not fully committed - FAIL **";
echo "** Please run 'npm ci', followed by 'npm dedupe' **";
echo "** This can usually be fixed with: **";
echo "** git checkout master -- npm-shrinkwrap.json **";
echo "** rm -rf node_modules **";
echo "** npm install && npm dedupe && npm install **";
exit 1;
fi

View File

@ -58,7 +58,7 @@ const getUpstreams = async () => {
const repoYaml = fs.readFileSync(__dirname + '/../repo.yml', 'utf8');
const yaml = await import('js-yaml');
const { upstream } = yaml.safeLoad(repoYaml) as {
const { upstream } = yaml.load(repoYaml) as {
upstream: Upstream[];
};

View File

@ -18,11 +18,25 @@
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, '..');
const nodeEngineWarn = `\
------------------------------------------------------------------------------
Warning: Node version "v14.x.x" does not match required versions ">=10.20.0 <13.0.0".
This may cause unexpected behavior. To upgrade Node, visit:
https://nodejs.org/en/download/
------------------------------------------------------------------------------
`;
const nodeEngineWarnArray = nodeEngineWarn.split('\n').filter((l) => l);
export function matchesNodeEngineVersionWarn(line: string) {
line = line.replace(/"v14\.\d{1,3}\.\d{1,3}"/, '"v14.x.x"');
return (
line === nodeEngineWarn || nodeEngineWarnArray.includes(line.trimEnd())
);
}
/** Tap and buffer this process' stdout and stderr */
export class StdOutTap {
public stdoutBuf: string[] = [];
@ -90,40 +104,6 @@ export function loadPackageJson() {
return require(path.join(ROOT, 'package.json'));
}
/**
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
*/
export function fixPathForMsys(p: string): string {
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
}
/**
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
* The given argv arguments are escaped using the 'shell-escape' package,
* so that backslashes in Windows paths, and other bash-special characters,
* are preserved. If argv is not provided, defaults to process.argv, to the
* effect that this current (parent) process is re-executed under MSYS2 bash.
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
* Windows.
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
*/
export async function runUnderMsys(argv?: string[]) {
const newArgv = argv || process.argv;
await new Promise((resolve, reject) => {
const args = ['-lc', shellEscape(newArgv)];
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
child.on('close', (code) => {
if (code) {
console.log(`runUnderMsys: child process exited with code ${code}`);
reject(code);
} else {
resolve();
}
});
});
}
/**
* Run the executable at execPath as a child process, and resolve a promise
* to the executable's stdout output as a string. Reject the promise if
@ -156,7 +136,8 @@ export async function getSubprocessStdout(
// every line provided to the stderr stream
const lines = _.filter(
stderr.trim().split(/\r?\n/),
(line) => !line.startsWith('[debug]'),
(line) =>
!line.startsWith('[debug]') && !matchesNodeEngineVersionWarn(line),
);
if (lines.length > 0) {
reject(

View File

@ -21,9 +21,14 @@ modifyOclifPaths();
process.on('exit', function () {
modifyOclifPaths(true);
});
// Undo changes in case of ctrl-v
// Undo changes in case of ctrl-c
process.on('SIGINT', function () {
modifyOclifPaths(true);
// Note process exit here will interfere with commands that do their own SIGINT handling,
// but without it commands can not be exited.
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
// Ideally a better solution is needed.
process.exit();
});
// Use fast-boot to cache require lookups, speeding up startup

File diff suppressed because it is too large Load Diff

View File

@ -20,9 +20,10 @@ import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import type * as BalenaSDK from 'balena-sdk';
import type { Application } from 'balena-sdk';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
@ -37,16 +38,26 @@ export default class AppCreateCmd extends Command {
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 specify the organization the application should belong to using
the \`--organization\` option. The organization's handle, not its name,
should be provided. Organization handles can be listed with the
\`balena orgs\` command.
You can see a list of supported device types with:
The application's default device type is specified with the \`--type\` option.
The \`balena devices supported\` command can be used to list the available
device types.
Interactive dropdowns will be shown for selection if no device type or
organization is specified and there are multiple options to choose from.
If there is a single option to choose from, it will be chosen automatically.
This interactive behavior can be disabled by explicitly specifying a device
type and organization.
`;
$ balena devices supported
`;
public static examples = [
'$ balena app create MyApp',
'$ balena app create MyApp --type raspberry-pi',
'$ balena app create MyApp --organization mmyorg',
'$ balena app create MyApp -o myorg --type raspberry-pi',
];
public static args = [
@ -60,6 +71,11 @@ export default class AppCreateCmd extends Command {
public static usage = 'app create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description:
'handle of the organization the application should belong to',
}),
type: flags.string({
char: 't',
description:
@ -75,30 +91,62 @@ export default class AppCreateCmd extends Command {
AppCreateCmd,
);
const balena = getBalenaSdk();
// Create application
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
let application: BalenaSDK.Application;
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
let application: Application;
try {
application = await balena.models.application.create({
application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization: (await balena.auth.whoami())!,
organization,
});
} catch (err) {
// BalenaRequestError: Request error: Unique key constraint violated
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: application "${params.name}" already exists`,
`Error: application "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create applications in organization "${organization}".`,
);
}
throw err;
}
console.info(
`Application created: ${application.slug} (${deviceType}, id ${application.id})`,
// Output
const { isV13 } = await import('../../utils/version');
console.log(
isV13()
? `Application created: slug "${application.slug}", device type "${deviceType}"`
: `Application created: ${application.slug} (${deviceType}, id ${application.id})`,
);
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk());
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
}
}

View File

@ -18,7 +18,9 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
import type { Release } from 'balena-sdk';
interface FlagsDef {
@ -26,7 +28,7 @@ interface FlagsDef {
}
interface ArgsDef {
name: string;
application: string;
}
export default class AppCmd extends Command {
@ -34,18 +36,14 @@ export default class AppCmd extends Command {
Display information about a single application.
Display detailed information about a single balena application.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = ['$ balena app MyApp'];
public static examples = ['$ balena app MyApp', '$ balena app myorg/myapp'];
public static args = [
{
name: 'name',
description: 'application name or numeric ID',
required: true,
},
];
public static args = [ca.applicationRequired];
public static usage = 'app <name>';
public static usage = 'app <nameOrSlug>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
@ -59,22 +57,29 @@ export default class AppCmd extends Command {
const { getApplication } = await import('../../utils/sdk');
const application = (await getApplication(getBalenaSdk(), params.name, {
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
const application = (await getApplication(
getBalenaSdk(),
params.application,
{
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
},
},
})) as ApplicationWithDeviceType & {
)) as ApplicationWithDeviceType & {
should_be_running__release: [Release?];
// For display purposes:
device_type: string;
commit?: string;
};
// @ts-expect-error
application.device_type = application.is_for__device_type[0].slug;
// @ts-expect-error
application.commit = application.should_be_running__release[0]?.commit;
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
console.log(`== ${application.app_name}`);
console.log(
getVisuals().table.vertical(application, [
`$${application.app_name}$`,
'id',
'device_type',
'slug',

View File

@ -18,35 +18,36 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
name: string;
application: string;
}
export default class AppRestartCmd extends Command {
export default class AppPurgeCmd extends Command {
public static description = stripIndent`
Purge data from an application.
Purge data from all devices belonging to an application.
This will clear the application's /data directory.
`;
public static examples = ['$ balena app purge MyApp'];
public static args = [
{
name: 'name',
description: 'application name or numeric ID',
required: true,
},
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena app purge MyApp',
'$ balena app purge myorg/myapp',
];
public static usage = 'app purge <name>';
public static args = [ca.applicationRequired];
public static usage = 'app purge <application>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
@ -55,21 +56,18 @@ export default class AppRestartCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// balena.models.application.purge only accepts a numeric id
// so we must first fetch the app to get it's id, if we have been given a name
let nameOrId = tryAsInteger(params.name);
if (typeof nameOrId === 'string') {
const app = await balena.models.application.get(nameOrId);
nameOrId = app.id;
}
// so we must first fetch the app to get it's id,
const application = await getApplication(balena, params.application);
try {
await balena.models.application.purge(nameOrId);
await balena.models.application.purge(application.id);
} catch (e) {
if (e.message.toLowerCase().includes('no online device(s) found')) {
// application.purge throws an error if no devices are online

View File

@ -16,18 +16,19 @@
*/
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
import { applicationIdInfo } from '../../utils/messages';
import type { ApplicationType } from 'balena-sdk';
interface FlagsDef {
help: void;
}
interface ArgsDef {
name: string;
application: string;
newName?: string;
}
@ -39,25 +40,25 @@ export default class AppRenameCmd extends Command {
Note, if the \`newName\` parameter is omitted, it will be
prompted for interactively.
`;
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena app rename OldName',
'$ balena app rename OldName NewName',
'$ balena app rename myorg/oldname NewName',
];
public static args: Array<IArg<any>> = [
{
name: 'name',
description: 'application name or numeric ID',
required: true,
},
public static args = [
ca.applicationRequired,
{
name: 'newName',
description: 'the new name for the application',
},
];
public static usage = 'app rename <name> [newName]';
public static usage = 'app rename <application> [newName]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
@ -68,38 +69,37 @@ export default class AppRenameCmd extends Command {
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
const { ExpectedError, instanceOf } = await import('../../errors');
const { getApplication } = await import('../../utils/sdk');
const { validateApplicationName } = await import('../../utils/validation');
const { ExpectedError } = await import('../../errors');
const balena = getBalenaSdk();
// Get app
let app;
try {
app = await getApplication(balena, params.name, {
$expand: {
application_type: {
$select: ['is_legacy'],
},
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, params.application, {
$expand: {
application_type: {
$select: ['is_legacy'],
},
});
} catch (e) {
const { BalenaApplicationNotFound } = await import('balena-errors');
if (instanceOf(e, BalenaApplicationNotFound)) {
throw new ExpectedError(`Application ${params.name} not found.`);
} else {
throw e;
}
}
},
});
// Check app supports renaming
const appType = (app.application_type as ApplicationType[])?.[0];
if (appType.is_legacy) {
// Check app exists
if (!application) {
throw new ExpectedError(
`Application ${params.name} is of 'legacy' type, and cannot be renamed.`,
'Error: application ${params.nameOrSlug} not found.',
);
}
const { validateApplicationName } = await import('../../utils/validation');
// Check app supports renaming
const appType = (application.application_type as ApplicationType[])?.[0];
if (appType.is_legacy) {
throw new ExpectedError(
`Application ${params.application} is of 'legacy' type, and cannot be renamed.`,
);
}
// Ascertain new name
const newName =
params.newName ||
(await getCliForm().ask({
@ -109,28 +109,31 @@ export default class AppRenameCmd extends Command {
})) ||
'';
// Rename
try {
await this.renameApplication(balena, app.id, newName);
await balena.models.application.rename(application.id, newName);
} catch (e) {
// BalenaRequestError: Request error: Unique key constraint violated
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
if ((e.message || '').toLowerCase().includes('unique')) {
throw new ExpectedError(
`Error: application ${params.name} already exists.`,
`Error: application ${params.application} already exists.`,
);
}
throw e;
}
console.log(`Application ${params.name} renamed to ${newName}`);
}
// Get application again, to be sure of results
const renamedApplication = await balena.models.application.get(
application.id,
);
async renameApplication(balena: BalenaSDK, id: number, newName: string) {
return balena.pine.patch<Application>({
resource: 'application',
id,
body: {
app_name: newName,
},
});
// Output result
console.log(`Application renamed`);
console.log('From:');
console.log(`\tname: ${application.app_name}`);
console.log(`\tslug: ${application.slug}`);
console.log('To:');
console.log(`\tname: ${renamedApplication.app_name}`);
console.log(`\tslug: ${renamedApplication.slug}`);
}
}

View File

@ -18,15 +18,16 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
name: string;
application: string;
}
export default class AppRestartCmd extends Command {
@ -34,18 +35,18 @@ export default class AppRestartCmd extends Command {
Restart an application.
Restart all devices belonging to an application.
`;
public static examples = ['$ balena app restart MyApp'];
public static args = [
{
name: 'name',
description: 'application name or numeric ID',
required: true,
},
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena app restart MyApp',
'$ balena app restart myorg/myapp',
];
public static usage = 'app restart <name>';
public static args = [ca.applicationRequired];
public static usage = 'app restart <application>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
@ -56,6 +57,13 @@ export default class AppRestartCmd extends Command {
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
await getBalenaSdk().models.application.restart(tryAsInteger(params.name));
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.application);
await balena.models.application.restart(application.id);
}
}

View File

@ -18,8 +18,9 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
yes: boolean;
@ -27,7 +28,7 @@ interface FlagsDef {
}
interface ArgsDef {
name: string;
application: string;
}
export default class AppRmCmd extends Command {
@ -37,21 +38,19 @@ export default class AppRmCmd extends Command {
Permanently remove a balena application.
The --yes option may be used to avoid interactive confirmation.
`;
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena app rm MyApp',
'$ balena app rm MyApp --yes',
'$ balena app rm myorg/myapp',
];
public static args = [
{
name: 'name',
description: 'application name or numeric ID',
required: true,
},
];
public static args = [ca.applicationRequired];
public static usage = 'app rm <name>';
public static usage = 'app rm <application>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
@ -65,15 +64,20 @@ export default class AppRmCmd extends Command {
AppRmCmd,
);
const patterns = await import('../../utils/patterns');
const { confirm } = await import('../../utils/patterns');
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// Confirm
await patterns.confirm(
await confirm(
options.yes ?? false,
`Are you sure you want to delete application ${params.name}?`,
`Are you sure you want to delete application ${params.application}?`,
);
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.application);
// Remove
await getBalenaSdk().models.application.remove(tryAsInteger(params.name));
await balena.models.application.remove(application.id);
}
}

View File

@ -19,7 +19,6 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { isV12 } from '../utils/version';
interface ExtendedApplication extends ApplicationWithDeviceType {
device_count?: number;
@ -28,7 +27,7 @@ interface ExtendedApplication extends ApplicationWithDeviceType {
interface FlagsDef {
help: void;
verbose?: boolean;
verbose: boolean;
}
export default class AppsCmd extends Command {
@ -38,8 +37,9 @@ export default class AppsCmd extends Command {
list all your balena applications.
For detailed information on a particular application,
use \`balena app <name> instead\`.
`;
use \`balena app <application>\` instead.
`;
public static examples = ['$ balena apps'];
public static usage = 'apps';
@ -47,10 +47,9 @@ export default class AppsCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
verbose: flags.boolean({
default: false,
char: 'v',
description: isV12()
? 'No-op since release v12.0.0'
: 'add extra columns in the tabular output (SLUG)',
description: 'No-op since release v12.0.0',
}),
};
@ -58,7 +57,7 @@ export default class AppsCmd extends Command {
public static primary = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(AppsCmd);
this.parse<FlagsDef, {}>(AppsCmd);
const balena = getBalenaSdk();
@ -87,7 +86,7 @@ export default class AppsCmd extends Command {
getVisuals().table.horizontal(applications, [
'id',
'app_name',
options.verbose || isV12() ? 'slug' : '',
'slug',
'device_type',
'online_devices',
'device_count',

View File

@ -20,7 +20,11 @@ import Command from '../command';
import { getBalenaSdk } from '../utils/lazy';
import * as compose from '../utils/compose';
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import {
buildArgDeprecation,
dockerignoreHelp,
registrySecretsHelp,
} from '../utils/messages';
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
import { buildProject, composeCliFlags } from '../utils/compose_ts';
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
@ -122,6 +126,11 @@ ${dockerignoreHelp}
await this.validateOptions(options, sdk);
// Build args are under consideration for removal - warn user
if (options.buildArg) {
console.log(buildArgDeprecation);
}
const app = await this.getAppAndResolveArch(options);
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);

View File

@ -19,6 +19,7 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
import type { PineDeferred } from 'balena-sdk';
interface FlagsDef {
@ -51,7 +52,9 @@ export default class ConfigGenerateCmd extends Command {
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.
you can pass the --deviceType argument along with --application to specify the target device type.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
@ -60,7 +63,8 @@ export default class ConfigGenerateCmd extends Command {
'$ 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 myorg/myapp --version 2.12.7',
'$ balena config generate --app MyApp --version 2.12.7 --deviceType fincm3',
'$ balena config generate --app MyApp --version 2.12.7 --output config.json',
'$ balena config generate --app MyApp --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1',
];
@ -72,15 +76,8 @@ export default class ConfigGenerateCmd extends Command {
description: 'a balenaOS version',
required: true,
}),
application: flags.string({
description: 'application name',
char: 'a',
exclusive: ['app', 'device'],
}),
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device'],
}),
application: { ...cf.application, exclusive: ['app', 'device'] },
app: { ...cf.app, exclusive: ['application', 'device'] },
device: flags.string({
description: 'device uuid',
char: 'd',
@ -154,6 +151,7 @@ export default class ConfigGenerateCmd extends Command {
};
resourceDeviceType = device.is_of__device_type[0].slug;
} else {
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
application = (await getApplication(balena, options.application!, {
$expand: {
is_for__device_type: { $select: 'slug' },
@ -227,17 +225,8 @@ export default class ConfigGenerateCmd extends Command {
$ balena help config generate
`;
protected readonly deviceTypeNotAllowedMessage = stripIndent`
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
`;
protected readonly deviceTypeNotAllowedMessage =
'The --deviceType option can only be used alongside the --application option';
protected async validateOptions(options: FlagsDef) {
const { ExpectedError } = await import('../../errors');

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -54,16 +54,8 @@ export default class ConfigInjectCmd extends Command {
public static usage = 'config inject <file>';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
drive: flags.string({
description: 'device filesystem or OS image location',
char: 'd',
}),
type: cf.deviceType,
drive: cf.driveOrImg,
help: cf.help,
};
@ -76,12 +68,11 @@ export default class ConfigInjectCmd extends Command {
ConfigInjectCmd,
);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { safeUmount } = await import('../../utils/helpers');
const drive =
options.drive || (await getVisuals().drive('Select the device/OS drive'));
await umountAsync(drive);
await safeUmount(drive);
const fs = await import('fs');
const configJSON = JSON.parse(

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -42,16 +42,8 @@ export default class ConfigReadCmd extends Command {
public static usage = 'config read';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
drive: flags.string({
description: 'device filesystem or OS image location',
char: 'd',
}),
type: cf.deviceType,
drive: cf.driveOrImg,
help: cf.help,
};
@ -62,12 +54,11 @@ export default class ConfigReadCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { safeUmount } = await import('../../utils/helpers');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));
await umountAsync(drive);
await safeUmount(drive);
const config = await import('balena-config-json');
const configJSON = await config.read(drive, options.type);

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -42,16 +42,8 @@ export default class ConfigReconfigureCmd extends Command {
public static usage = 'config reconfigure';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
drive: flags.string({
description: 'device filesystem or OS image location',
char: 'd',
}),
type: cf.deviceType,
drive: cf.driveOrImg,
advanced: flags.boolean({
description: 'show advanced commands',
char: 'v',
@ -66,16 +58,15 @@ export default class ConfigReconfigureCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { safeUmount } = await import('../../utils/helpers');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));
await umountAsync(drive);
await safeUmount(drive);
const config = await import('balena-config-json');
const { uuid } = await config.read(drive, options.type);
await umountAsync(drive);
await safeUmount(drive);
const configureCommand = ['os', 'configure', drive, '--device', uuid];
if (options.advanced) {

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -61,16 +61,8 @@ export default class ConfigWriteCmd extends Command {
public static usage = 'config write <key> <value>';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
drive: flags.string({
description: 'device filesystem or OS image location',
char: 'd',
}),
type: cf.deviceType,
drive: cf.driveOrImg,
help: cf.help,
};
@ -83,12 +75,11 @@ export default class ConfigWriteCmd extends Command {
ConfigWriteCmd,
);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { safeUmount } = await import('../../utils/helpers');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));
await umountAsync(drive);
await safeUmount(drive);
const config = await import('balena-config-json');
const configJSON = await config.read(drive, options.type);
@ -97,7 +88,7 @@ export default class ConfigWriteCmd extends Command {
const _ = await import('lodash');
_.set(configJSON, params.key, params.value);
await umountAsync(drive);
await safeUmount(drive);
await config.write(drive, options.type, configJSON);

View File

@ -20,22 +20,34 @@ import type { ImageDescriptor } from 'resin-compose-parse';
import Command from '../command';
import { ExpectedError } from '../errors';
import { getBalenaSdk, getChalk } from '../utils/lazy';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
import {
dockerignoreHelp,
registrySecretsHelp,
buildArgDeprecation,
} from '../utils/messages';
import * as compose from '../utils/compose';
import type {
BuiltImage,
ComposeCliFlags,
ComposeOpts,
Release as ComposeReleaseInfo,
} from '../utils/compose-types';
import type { DockerCliFlags } from '../utils/docker';
import {
applyReleaseTagKeysAndValues,
buildProject,
composeCliFlags,
isBuildConfig,
parseReleaseTagKeysAndValues,
} from '../utils/compose_ts';
import { dockerCliFlags } from '../utils/docker';
import type { Application, ApplicationType, DeviceType } from 'balena-sdk';
import type {
Application,
ApplicationType,
DeviceType,
Release,
} from 'balena-sdk';
interface ApplicationWithArch extends Application {
arch: string;
@ -45,6 +57,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
source?: string;
build: boolean;
nologupload: boolean;
'release-tag'?: string[];
help: void;
}
@ -85,6 +98,7 @@ ${dockerignoreHelp}
'$ balena deploy myApp',
'$ balena deploy myApp --build --source myBuildDir/',
'$ balena deploy myApp myApp/myImage',
'$ balena deploy myApp myApp/myImage --release-tag key1 "" key2 "value2 with spaces"',
];
public static args = [
@ -115,6 +129,14 @@ ${dockerignoreHelp}
description:
"don't upload build logs to the dashboard with image (if building)",
}),
'release-tag': flags.string({
description: stripIndent`
Set release tags if the image deployment is successful. Multiple
arguments may be provided, alternating tag keys and values (see examples).
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
`,
multiple: true,
}),
...composeCliFlags,
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
@ -140,6 +162,11 @@ ${dockerignoreHelp}
const { appName, image } = params;
// Build args are under consideration for removal - warn user
if (options.buildArg) {
console.log(buildArgDeprecation);
}
if (image != null && options.build) {
throw new ExpectedError(
'Build option is not applicable when specifying an image',
@ -151,6 +178,10 @@ ${dockerignoreHelp}
'../utils/compose_ts'
);
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
options['release-tag'] ?? [],
);
if (image) {
options['registry-secrets'] = await getRegistrySecrets(
sdk,
@ -180,7 +211,7 @@ ${dockerignoreHelp}
compose.generateOpts(options),
]);
await this.deployProject(docker, logger, composeOpts, {
const release = await this.deployProject(docker, logger, composeOpts, {
app,
appName, // may be prefixed by 'owner/', unlike app.app_name
image,
@ -189,6 +220,12 @@ ${dockerignoreHelp}
buildEmulated: !!options.emulated,
buildOpts,
});
await applyReleaseTagKeysAndValues(
sdk,
release.id,
releaseTagKeys,
releaseTagValues,
);
}
async deployProject(
@ -286,7 +323,7 @@ ${dockerignoreHelp}
},
);
let release;
let release: Release | ComposeReleaseInfo['release'];
if (appType?.is_legacy) {
const { deployLegacy } = require('../utils/deploy-legacy');
@ -344,6 +381,7 @@ ${dockerignoreHelp}
console.log();
console.log(doodles.getDoodle()); // Show charlie
console.log();
return release;
} catch (err) {
logger.logError('Deploy failed');
throw err;

View File

@ -0,0 +1,87 @@
/**
* @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 type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceDeactivateCmd extends Command {
public static description = stripIndent`
Deactivate a device.
Deactivate a device.
Note this command asks for confirmation interactively.
You can avoid this by passing the \`--yes\` option.
`;
public static examples = [
'$ balena device deactivate 7cf02a6',
'$ balena device deactivate 7cf02a6 --yes',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the UUID of the device to be deactivated',
required: true,
},
];
public static usage = 'device deactivate <uuid>';
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>(
DeviceDeactivateCmd,
);
const balena = getBalenaSdk();
const patterns = await import('../../utils/patterns');
const uuid = params.uuid;
const deactivationWarning = `
Warning! Deactivating a device will charge a fee equivalent to the
normal monthly cost for the device (e.g. $1 for an essentials device);
the device will not be charged again until it comes online.
`;
const warning = `Are you sure you want to deactivate device ${uuid} ?`;
console.error(deactivationWarning);
// Confirm
await patterns.confirm(options.yes, warning);
// Deactivate
await balena.models.device.deactivate(uuid);
}
}

View File

@ -96,6 +96,7 @@ export default class DeviceCmd extends Command {
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
@ -168,6 +169,7 @@ export default class DeviceCmd extends Command {
'status',
'is_online',
'ip_address',
'public_address',
'mac_address',
'application_name',
'last_seen',

View File

@ -19,6 +19,7 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
import { runCommand } from '../../utils/helpers';
interface FlagsDef {
@ -34,17 +35,21 @@ interface FlagsDef {
export default class DeviceInitCmd extends Command {
public static description = stripIndent`
Initialise a device with balenaOS.
Initialize a device with balenaOS.
Initialise a device by downloading the OS image of a certain application
Initialize a device by downloading the OS image of a certain application
and writing it to an SD Card.
Note, if the application option is omitted it will be prompted
for interactively.
`;
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena device init',
'$ balena device init --application MyApp',
'$ balena device init -a myorg/myapp',
];
public static usage = 'device init';
@ -98,7 +103,7 @@ export default class DeviceInitCmd extends Command {
const application = (await getApplication(
balena,
options['application'] ||
(await (await import('../../utils/patterns')).selectApplication()),
(await (await import('../../utils/patterns')).selectApplication()).id,
{
$expand: {
is_for__device_type: {

View File

@ -0,0 +1,114 @@
/**
* @license
* Copyright 2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
enable: boolean;
disable: boolean;
status: boolean;
help?: void;
}
interface ArgsDef {
uuid: string | number;
}
export default class DeviceLocalModeCmd extends Command {
public static description = stripIndent`
Get or manage the local mode status for a device.
Output current local mode status, or enable/disable local mode
for specified device.
`;
public static examples = [
'$ balena device local-mode 23c73a1',
'$ balena device local-mode 23c73a1 --enable',
'$ balena device local-mode 23c73a1 --disable',
'$ balena device local-mode 23c73a1 --status',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to manage',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
public static usage = 'device local-mode <uuid>';
public static flags: flags.Input<FlagsDef> = {
enable: flags.boolean({
description: 'enable local mode',
exclusive: ['disable', 'status'],
}),
disable: flags.boolean({
description: 'disable local mode',
exclusive: ['enable', 'status'],
}),
status: flags.boolean({
description: 'output boolean indicating local mode status',
exclusive: ['enable', 'disable'],
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceLocalModeCmd,
);
const balena = getBalenaSdk();
if (options.enable) {
await balena.models.device.enableLocalMode(params.uuid);
console.log(`Local mode on device ${params.uuid} is now ENABLED.`);
} else if (options.disable) {
await balena.models.device.disableLocalMode(params.uuid);
console.log(`Local mode on device ${params.uuid} is now DISABLED.`);
} else if (options.status) {
// Output bool indicating local mode status
const isEnabled = await balena.models.device.isInLocalMode(params.uuid);
console.log(isEnabled);
} else {
// If no flag provided, output status and tip
const isEnabled = await balena.models.device.isInLocalMode(params.uuid);
console.log(
`Local mode on device ${params.uuid} is ${
isEnabled ? 'ENABLED' : 'DISABLED'
}.`,
);
if (isEnabled) {
console.log('To disable, use:');
console.log(` balena device local-mode ${params.uuid} --disable`);
} else {
console.log('To enable, use:');
console.log(` balena device local-mode ${params.uuid} --enable`);
}
}
}
}

View File

@ -20,9 +20,8 @@ import type { IArg } from '@oclif/parser/lib/args';
import type { Application, BalenaSDK } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { applicationIdInfo } from '../../utils/messages';
import { ExpectedError } from '../../errors';
interface ExtendedDevice extends DeviceWithDeviceType {
@ -47,11 +46,15 @@ export default class DeviceMoveCmd extends Command {
Note, if the application option is omitted it will be prompted
for interactively.
`;
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena device move 7cf02a6',
'$ balena device move 7cf02a6,dc39e52',
'$ balena device move 7cf02a6 --application MyNewApp',
'$ balena device move 7cf02a6 -a myorg/mynewapp',
];
public static args: Array<IArg<any>> = [
@ -80,6 +83,9 @@ export default class DeviceMoveCmd extends Command {
const balena = getBalenaSdk();
const { tryAsInteger } = await import('../../utils/validation');
const { expandForAppName } = await import('../../utils/helpers');
options.application = options.application || options.app;
delete options.app;
@ -106,16 +112,21 @@ export default class DeviceMoveCmd extends Command {
: 'N/a';
}
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../../utils/sdk');
// Get destination application
const application =
options.application ||
(await this.interactivelySelectApplication(balena, devices));
const application = options.application
? await getApplication(balena, options.application)
: await this.interactivelySelectApplication(balena, devices);
// Move each device
for (const uuid of deviceIds) {
try {
await balena.models.device.move(uuid, tryAsInteger(application));
console.info(`${uuid} was moved to ${application}`);
await balena.models.device.move(uuid, application.id);
console.info(
`Device ${uuid} was moved to application ${application.slug}`,
);
} catch (err) {
console.info(`${err.message}, uuid: ${uuid}`);
process.exitCode = 1;

View File

@ -19,7 +19,9 @@ import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
uuid?: string;
@ -35,19 +37,17 @@ export default class DeviceRegisterCmd extends Command {
Register a device.
Register a device to an application.
`;
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena device register MyApp',
'$ balena device register MyApp --uuid <uuid>',
'$ balena device register myorg/myapp --uuid <uuid>',
];
public static args: Array<IArg<any>> = [
{
name: 'application',
description: 'the name or id of application to register device with',
required: true,
},
];
public static args: Array<IArg<any>> = [ca.applicationRequired];
public static usage = 'device register <application>';

View File

@ -20,7 +20,7 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
import type { Application } from 'balena-sdk';
interface ExtendedDevice extends DeviceWithDeviceType {
@ -44,17 +44,16 @@ export default class DevicesCmd extends Command {
You can filter the devices by application by using the \`--application\` option.
The --json option is recommended when scripting the output of this command,
because field names are less likely to change in JSON format and because it
better represents data types like arrays, empty strings and null values.
The 'jq' utility may be helpful for querying JSON fields in shell scripts
(https://stedolan.github.io/jq/manual/).
${applicationIdInfo.split('\n').join('\n\t\t')}
${jsonInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena devices',
'$ balena devices --application MyApp',
'$ balena devices --app MyApp',
'$ balena devices -a MyApp',
'$ balena devices -a myorg/myapp',
];
public static usage = 'devices';
@ -62,11 +61,8 @@ export default class DevicesCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
app: cf.app,
json: cf.json,
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
};
public static primary = true;
@ -85,8 +81,10 @@ export default class DevicesCmd extends Command {
let devices;
if (options.application != null) {
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, options.application);
devices = (await balena.models.device.getAllByApplication(
tryAsInteger(options.application),
application.id,
expandForAppName,
)) as ExtendedDevice[];
} else {
@ -101,7 +99,7 @@ export default class DevicesCmd extends Command {
const belongsToApplication = device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]?.app_name || null;
device.uuid = device.uuid.slice(0, 7);
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
device.device_type = device.is_of__device_type?.[0]?.slug || null;
return device;

View File

@ -18,13 +18,13 @@
import { flags } from '@oclif/command';
import type * as BalenaSdk from 'balena-sdk';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
application?: string; // application name
application?: string;
device?: string; // device UUID
help: void;
quiet: boolean;
@ -63,10 +63,14 @@ export default class EnvAddCmd extends Command {
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.
`;
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena env add TERM --application MyApp',
'$ balena env add EDITOR vim --application MyApp',
'$ balena env add EDITOR vim -a myorg/myapp',
'$ balena env add EDITOR vim --application MyApp,MyApp2',
'$ balena env add EDITOR vim --application MyApp --service MyService',
'$ balena env add EDITOR vim --application MyApp,MyApp2 --service MyService,MyService2',
@ -93,8 +97,8 @@ export default class EnvAddCmd extends Command {
public static usage = 'env add <name> [value]';
public static flags: flags.Input<FlagsDef> = {
application: { exclusive: ['device'], ...cf.application },
device: { exclusive: ['application'], ...cf.device },
application: { ...cf.application, exclusive: ['device'] },
device: { ...cf.device, exclusive: ['application'] },
help: cf.help,
quiet: cf.quiet,
service: cf.service,
@ -108,7 +112,7 @@ export default class EnvAddCmd extends Command {
if (!options.application && !options.device) {
throw new ExpectedError(
'Either the --application or the --device option must always be used',
'Either the --application or the --device option must be specified',
);
}

View File

@ -18,16 +18,14 @@ import { flags } from '@oclif/command';
import type * as SDK from 'balena-sdk';
import * as _ from 'lodash';
import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { CommandHelp } from '../utils/oclif-utils';
import { isV12 } from '../utils/version';
import { applicationIdInfo } from '../utils/messages';
import { isV13 } from '../utils/version';
interface FlagsDef {
all?: boolean; // whether to include application-wide, device-wide variables //TODO: REMOVE
application?: string; // application name
application?: string;
config: boolean;
device?: string; // device UUID
json: boolean;
@ -57,8 +55,7 @@ interface ServiceEnvironmentVariableInfo
}
export default class EnvsCmd extends Command {
public static description = isV12()
? stripIndent`
public static description = stripIndent`
List the environment or config variables of an application, device or service.
List the environment or configuration variables of an application, device or
@ -91,99 +88,47 @@ export default class EnvsCmd extends Command {
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.)
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
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.
public static examples = [
'$ balena envs --application MyApp',
'$ balena envs --application myorg/myapp',
'$ balena envs --application MyApp --json',
'$ balena envs --application MyApp --service MyService',
'$ balena envs --application MyApp --service MyService',
'$ balena envs --application MyApp --config',
'$ balena envs --device 7cf02a6',
'$ balena envs --device 7cf02a6 --json',
'$ balena envs --device 7cf02a6 --config --json',
'$ balena envs --device 7cf02a6 --service MyService',
];
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 = 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 usage = 'envs';
public static flags: flags.Input<FlagsDef> = {
...(isV12()
? {
...(isV13()
? {}
: {
all: flags.boolean({
default: false,
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({
default: false,
char: 'c',
description: 'show configuration variables only',
exclusive: ['service'],
}),
device: { exclusive: ['application'], ...cf.device },
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
json: cf.json,
verbose: cf.verbose,
service: { exclusive: ['config'], ...cf.service },
};
@ -192,8 +137,6 @@ export default class EnvsCmd extends Command {
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
const variables: EnvironmentVariableInfo[] = [];
options.all = options.all || isV12();
await Command.checkLoggedIn();
if (!options.application && !options.device) {
@ -202,7 +145,7 @@ export default class EnvsCmd extends Command {
const balena = getBalenaSdk();
let appName = options.application;
let appNameOrSlug = options.application;
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
if (options.device) {
@ -215,18 +158,16 @@ export default class EnvsCmd extends Command {
);
fullUUID = device.uuid;
if (app) {
appName = app.app_name;
appNameOrSlug = 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 (appNameOrSlug && options.service) {
await validateServiceName(balena, options.service, appNameOrSlug);
}
variables.push(...(await getAppVars(balena, appNameOrSlug, options)));
if (fullUUID) {
variables.push(
...(await getDeviceVars(balena, fullUUID, appName, options)),
...(await getDeviceVars(balena, fullUUID, appNameOrSlug, options)),
);
}
if (!options.json && variables.length === 0) {
@ -247,20 +188,18 @@ export default class EnvsCmd extends Command {
) {
const fields = ['id', 'name', 'value'];
if (options.all) {
// Replace undefined app names with 'N/A' or null
varArray = varArray.map((i: EnvironmentVariableInfo) => {
i.appName = i.appName || (options.json ? null : 'N/A');
return i;
});
// Replace undefined app names with 'N/A' or null
varArray = varArray.map((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');
}
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) {
@ -302,21 +241,19 @@ async function validateServiceName(
*/
async function getAppVars(
sdk: SDK.BalenaSDK,
appName: string | undefined,
appNameOrSlug: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const appVars: EnvironmentVariableInfo[] = [];
if (!appName) {
if (!appNameOrSlug) {
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 vars = await sdk.models.application[
options.config ? 'configVar' : 'envVar'
].getAllByApplication(appNameOrSlug);
fillInInfoFields(vars, appNameOrSlug);
appVars.push(...vars);
if (!options.config) {
const pineOpts: SDK.PineOptions<SDK.ServiceEnvironmentVariable> = {
$expand: {
service: {},
@ -330,10 +267,10 @@ async function getAppVars(
};
}
const serviceVars = await sdk.models.service.var.getAllByApplication(
appName,
appNameOrSlug,
pineOpts,
);
fillInInfoFields(serviceVars, appName);
fillInInfoFields(serviceVars, appNameOrSlug);
appVars.push(...serviceVars);
}
return appVars;
@ -346,7 +283,7 @@ async function getAppVars(
async function getDeviceVars(
sdk: SDK.BalenaSDK,
fullUUID: string,
appName: string | undefined,
appNameOrSlug: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const printedUUID = options.json ? fullUUID : options.device!;
@ -355,38 +292,35 @@ async function getDeviceVars(
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceConfigVars, appName, printedUUID);
fillInInfoFields(deviceConfigVars, appNameOrSlug, printedUUID);
deviceVars.push(...deviceConfigVars);
} else {
if (options.service || options.all) {
const pineOpts: SDK.PineOptions<SDK.DeviceServiceEnvironmentVariable> = {
$expand: {
service_install: {
$expand: 'installs__service',
},
const pineOpts: SDK.PineOptions<SDK.DeviceServiceEnvironmentVariable> = {
$expand: {
service_install: {
$expand: 'installs__service',
},
},
};
if (options.service) {
pineOpts.$filter = {
service_install: {
installs__service: { service_name: options.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);
}
const deviceServiceVars = await sdk.models.device.serviceVar.getAllByDevice(
fullUUID,
pineOpts,
);
fillInInfoFields(deviceServiceVars, appNameOrSlug, printedUUID);
deviceVars.push(...deviceServiceVars);
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceEnvVars, appNameOrSlug, printedUUID);
deviceVars.push(...deviceEnvVars);
}
return deviceVars;
}
@ -401,7 +335,7 @@ function fillInInfoFields(
| EnvironmentVariableInfo[]
| DeviceServiceEnvironmentVariableInfo[]
| ServiceEnvironmentVariableInfo[],
appName?: string,
appNameOrSlug?: string,
deviceUUID?: string,
) {
for (const envVar of varArray) {
@ -413,7 +347,7 @@ function fillInInfoFields(
envVar.serviceName = ((envVar.service_install as SDK.ServiceInstall[])[0]
?.installs__service as SDK.Service[])[0]?.service_name;
}
envVar.appName = appName;
envVar.appName = appNameOrSlug;
envVar.serviceName = envVar.serviceName || '*';
envVar.deviceUUID = deviceUUID || '*';
}

View File

@ -1,55 +0,0 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Command from '../../command';
import { stripIndent } from '../../utils/lazy';
// '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');
try {
const hostnameOrIp = await forms.selectLocalBalenaOsDevice();
return console.error(`==> Selected device: ${hostnameOrIp}`);
} catch (e) {
if (e.message.toLowerCase().includes('could not find any')) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(e);
} else {
throw e;
}
}
}
}

View File

@ -19,6 +19,7 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import { parseAsLocalHostnameOrIp } from '../utils/validation';
interface FlagsDef {
@ -47,14 +48,17 @@ export default class JoinCmd extends Command {
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.
from an interactive picker. This may require administrator/root privileges.
Likewise, if the application flag is not provided then a picker will be shown.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena join',
'$ balena join balena.local',
'$ balena join balena.local --application MyApp',
'$ balena join balena.local -a myorg/myapp',
'$ balena join 192.168.1.25',
'$ balena join 192.168.1.25 --application MyApp',
];
@ -71,10 +75,7 @@ export default class JoinCmd extends Command {
public static usage = 'join [deviceIpOrHostname]';
public static flags: flags.Input<FlagsDef> = {
application: {
description: 'the name of the application the device should join',
...cf.application,
},
application: cf.application,
pollInterval: flags.integer({
description: 'the interval in minutes to check for updates',
char: 'i',

View File

@ -18,7 +18,7 @@
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { stripIndent } from '../utils/lazy';
import { parseAsLocalHostnameOrIp } from '../utils/validation';
interface FlagsDef {
@ -42,7 +42,7 @@ export default class LeaveCmd extends Command {
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.
from an interactive picker. This may require administrator/root privileges.
`;
public static examples = [
@ -72,8 +72,7 @@ export default class LeaveCmd extends Command {
const { args: params } = this.parse<FlagsDef, ArgsDef>(LeaveCmd);
const promote = await import('../utils/promote');
const sdk = getBalenaSdk();
const logger = await Command.getLogger();
return promote.leave(logger, sdk, params.deviceIpOrHostname);
return promote.leave(logger, params.deviceIpOrHostname);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
*/
import { flags } from '@oclif/command';
import { promisify } from 'util';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
@ -59,22 +60,17 @@ export default class LocalConfigureCmd extends Command {
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
const { promisify } = await import('util');
const path = await import('path');
const umount = await import('umount');
const umountAsync = promisify(umount.umount);
const isMountedAsync = promisify(umount.isMounted);
const reconfix = await import('reconfix');
const denymount = promisify(await import('denymount'));
const { safeUmount } = await import('../../utils/helpers');
const Logger = await import('../../utils/logger');
const logger = Logger.getLogger();
const configurationSchema = await this.prepareConnectionFile(params.target);
if (await isMountedAsync(params.target)) {
await umountAsync(params.target);
}
await safeUmount(params.target);
const dmOpts: any = {};
if (process.pkg) {
@ -253,31 +249,33 @@ export default class LocalConfigureCmd extends Command {
*/
async prepareConnectionFile(target: string) {
const _ = await import('lodash');
const imagefs = await import('resin-image-fs');
const imagefs = await import('balena-image-fs');
const files = await imagefs.listDirectory({
image: target,
partition: this.BOOT_PARTITION,
path: this.CONNECTIONS_FOLDER,
});
const files = await imagefs.interact(
target,
this.BOOT_PARTITION,
async (_fs) => {
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
},
);
let connectionFileName;
if (_.includes(files, 'resin-wifi')) {
// The required file already exists, nothing to do
} else if (_.includes(files, 'resin-sample.ignore')) {
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
await imagefs.copy(
{
image: target,
partition: this.BOOT_PARTITION,
path: `${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
},
{
image: target,
partition: this.BOOT_PARTITION,
path: `${this.CONNECTIONS_FOLDER}/resin-wifi`,
},
);
await imagefs.interact(target, this.BOOT_PARTITION, async (_fs) => {
const readFileAsync = promisify(_fs.readFile);
const writeFileAsync = promisify(_fs.writeFile);
const contents = await readFileAsync(
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
{ encoding: 'utf8' },
);
return await writeFileAsync(
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
contents,
);
});
} else if (_.includes(files, 'resin-sample')) {
// Legacy mode, to be removed later
// We return the file name override from this branch
@ -289,14 +287,12 @@ export default class LocalConfigureCmd extends Command {
connectionFileName = 'resin-sample';
} else {
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
await imagefs.writeFile(
{
image: target,
partition: this.BOOT_PARTITION,
path: `${this.CONNECTIONS_FOLDER}/resin-wifi`,
},
this.CONNECTION_FILE,
);
await imagefs.interact(target, this.BOOT_PARTITION, async (_fs) => {
return await promisify(_fs.writeFile)(
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
this.CONNECTION_FILE,
);
});
}
return await this.getConfigurationSchema(connectionFileName);
}

View File

@ -16,6 +16,7 @@
*/
import { flags } from '@oclif/command';
import type { BlockDevice } from 'etcher-sdk/build/source-destination';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
@ -25,7 +26,6 @@ import {
getVisuals,
stripIndent,
} from '../../utils/lazy';
import type * as SDK from 'etcher-sdk';
interface FlagsDef {
yes: boolean;
@ -75,6 +75,23 @@ export default class LocalFlashCmd extends Command {
LocalFlashCmd,
);
if (process.platform === 'linux') {
const { promisify } = await import('util');
const { exec } = await import('child_process');
const execAsync = promisify(exec);
let distroVersion = '';
try {
const info = await execAsync('cat /proc/version');
distroVersion = info.stdout.toLowerCase();
// tslint:disable-next-line: no-empty
} catch {}
if (distroVersion.includes('microsoft')) {
throw new ExpectedError(stripIndent`
This command is known not to work on WSL. Please use a CLI release
for Windows (not WSL), or balenaEtcher.`);
}
}
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const drive = await this.getDrive(options);
@ -93,10 +110,9 @@ export default class LocalFlashCmd extends Command {
process.exit(0);
}
const file = new sourceDestination.File(
params.image,
sourceDestination.File.OpenFlags.Read,
);
const file = new sourceDestination.File({
path: params.image,
});
const source = await file.getInnerSource();
const visuals = getVisuals();
@ -105,29 +121,37 @@ export default class LocalFlashCmd extends Command {
verifying: new visuals.Progress('Validating'),
};
await multiWrite.pipeSourceToDestinations(
await multiWrite.pipeSourceToDestinations({
source,
[drive],
(_, error) => {
// onFail
console.log(getChalk().red.bold(error.message));
destinations: [drive],
onFail: (_, error) => {
console.error(getChalk().red.bold(error.message));
if (error.message.includes('EACCES')) {
console.error(
getChalk().red.bold(
'Try running this command with elevated privileges, with sudo or in a shell running with admininstrator privileges.',
),
);
}
},
(progress: SDK.multiWrite.MultiDestinationProgress) => {
// onProgress
onProgress: (progress) => {
progressBars[progress.type].update(progress);
},
true, // verify
);
verify: true,
});
}
async getDrive(options: {
drive?: string;
}): Promise<SDK.sourceDestination.BlockDevice> {
async getDrive(options: { drive?: string }): Promise<BlockDevice> {
const drive = options.drive || (await getVisuals().drive('Select a drive'));
const sdk = await import('etcher-sdk');
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter({
includeSystemDrives: () => false,
unmountOnSuccess: false,
write: true,
direct: true,
});
const scanner = new sdk.scanner.Scanner([adapter]);
await scanner.start();
try {

View File

@ -72,16 +72,19 @@ export default class LoginCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
web: flags.boolean({
default: false,
char: 'w',
description: 'web-based login',
exclusive: ['token', 'credentials'],
}),
token: flags.boolean({
default: false,
char: 't',
description: 'session token or API key',
exclusive: ['web', 'credentials'],
}),
credentials: flags.boolean({
default: false,
char: 'c',
description: 'credential-based login',
exclusive: ['web', 'token'],

View File

@ -23,6 +23,7 @@ import { LogMessage } from 'balena-sdk';
import { IArg } from '@oclif/parser/lib/args';
interface FlagsDef {
'max-retry'?: number;
tail?: boolean;
service?: string[];
system?: boolean;
@ -33,6 +34,8 @@ interface ArgsDef {
device: string;
}
const MAX_RETRY = 1000;
export default class LogsCmd extends Command {
public static description = stripIndent`
Show device logs.
@ -75,7 +78,13 @@ export default class LogsCmd extends Command {
public static usage = 'logs <device>';
public static flags: flags.Input<FlagsDef> = {
'max-retry': flags.integer({
description: stripIndent`
Maximum number of reconnection attempts on "connection lost" errors
(use 0 to disable auto reconnection).`,
}),
tail: flags.boolean({
default: false,
description: 'continuously stream output',
char: 't',
}),
@ -87,6 +96,7 @@ export default class LogsCmd extends Command {
multiple: true,
}),
system: flags.boolean({
default: false,
description:
'Only show system logs. This can be used in combination with --service.',
char: 'S',
@ -103,7 +113,7 @@ export default class LogsCmd extends Command {
const balena = getBalenaSdk();
const { serviceIdToName } = await import('../utils/cloud');
const { displayDeviceLogs, displayLogObject } = await import(
const { connectAndDisplayDeviceLogs, displayLogObject } = await import(
'../utils/device/logs'
);
const { validateIPAddress, validateDotLocalUrl } = await import(
@ -151,13 +161,13 @@ export default class LogsCmd extends Command {
}
logger.logDebug('Streaming logs');
const logStream = await deviceApi.getLogStream();
await displayDeviceLogs(
logStream,
await connectAndDisplayDeviceLogs({
deviceApi,
logger,
options.system || false,
options.service,
);
system: options.system || false,
filterServices: options.service,
maxAttempts: 1 + (options['max-retry'] ?? MAX_RETRY),
});
} else {
// Logs from cloud
await Command.checkLoggedIn();

56
lib/commands/orgs.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 Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
interface FlagsDef {
help: void;
}
export default class OrgsCmd extends Command {
public static description = stripIndent`
List all organizations.
list all the organizations that you are a member of.
`;
public static examples = ['$ balena orgs'];
public static usage = 'orgs';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
this.parse<FlagsDef, {}>(OrgsCmd);
const { getOwnOrganizations } = await import('../utils/sdk');
// Get organizations
const organizations = await getOwnOrganizations(getBalenaSdk());
// Display
console.log(
getVisuals().table.horizontal(organizations, ['name', 'handle']),
);
}
}

View File

@ -17,21 +17,21 @@
import { flags } from '@oclif/command';
import type * as BalenaSdk from 'balena-sdk';
import { promisify } from 'util';
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, stripIndent, getCliForm } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
const BOOT_PARTITION = 1;
const CONNECTIONS_FOLDER = '/system-connections';
interface FlagsDef {
advanced?: boolean;
app?: string;
application?: string;
app?: string;
config?: string;
'config-app-update-poll-interval'?: number;
'config-network'?: string;
@ -84,19 +84,23 @@ export default class OsConfigureCmd extends Command {
WiFi or ethernet connections. This option may be passed multiple times in case there
are multiple files to inject. See connection profile examples and reference at:
https://www.balena.io/docs/reference/OS/network/2.x/
https://developer.gnome.org/NetworkManager/stable/nm-settings.html
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
${applicationIdInfo.split('\n').join('\n\t\t')}
Note: This command is currently not supported on Windows natively. Windows users
are advised to install the Windows Subsystem for Linux (WSL) with Ubuntu, and use
the Linux release of the balena CLI:
https://docs.microsoft.com/en-us/windows/wsl/about
`;
public static examples = [
'$ balena os configure ../path/rpi3.img --device 7cf02a6',
'$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>',
'$ balena os configure ../path/rpi3.img --app MyApp',
'$ balena os configure ../path/rpi3.img -a myorg/myapp',
'$ balena os configure ../path/rpi3.img --app MyApp --version 2.12.7',
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3',
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3 --config myWifiConfig.json',
@ -118,11 +122,8 @@ export default class OsConfigureCmd extends Command {
description:
'ask advanced configuration questions (when in interactive mode)',
}),
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device'],
}),
application: { exclusive: ['app', 'device'], ...cf.application },
application: { ...cf.application, exclusive: ['app', 'device'] },
app: { ...cf.app, exclusive: ['application', 'device'] },
config: flags.string({
description:
'path to a pre-generated config.json file to be injected in the OS image',
@ -155,7 +156,6 @@ export default class OsConfigureCmd extends Command {
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"',
}),
@ -166,6 +166,7 @@ export default class OsConfigureCmd extends Command {
description:
"paths to local files to place into the 'system-connections' directory",
}),
help: cf.help,
};
public async run() {
@ -174,7 +175,7 @@ export default class OsConfigureCmd extends Command {
);
// Prefer options.application over options.app
options.application = options.application || options.app;
options.app = undefined;
delete options.app;
await validateOptions(options);
@ -266,6 +267,8 @@ export default class OsConfigureCmd extends Command {
);
if (options['system-connection']) {
const path = await import('path');
const files = await Promise.all(
options['system-connection'].map(async (filePath) => {
const content = await fs.readFile(filePath, 'utf8');
@ -277,17 +280,16 @@ export default class OsConfigureCmd extends Command {
};
}),
);
const imagefs = await import('resin-image-fs');
const imagefs = await import('balena-image-fs');
for (const { name, content } of files) {
await imagefs.writeFile(
{
image,
partition: BOOT_PARTITION,
path: path.join(CONNECTIONS_FOLDER, name),
},
content,
);
await imagefs.interact(image, BOOT_PARTITION, async (_fs) => {
return await promisify(_fs.writeFile)(
path.join(CONNECTIONS_FOLDER, name),
content,
);
});
console.info(`Copied system-connection file: ${name}`);
}
}
@ -295,19 +297,6 @@ export default class OsConfigureCmd extends Command {
}
async function validateOptions(options: FlagsDef) {
if (process.platform === 'win32') {
throw new ExpectedError(stripIndent`
Unsupported platform error: the 'balena os configure' command currently requires
the Windows Subsystem for Linux in order to run on Windows. It was tested with
the Ubuntu 18.04 distribution from the Microsoft Store. With WSL, a balena CLI
release for Linux (rather than Windows) should be installed: for example, the
standalone zip package for Linux. (It is possible to have both a Windows CLI
release and a Linux CLI release installed simultaneously.) For more information
on WSL and the balena CLI installation options, please check:
- https://docs.microsoft.com/en-us/windows/wsl/about
- https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
`);
}
// The 'device' and 'application' options are declared "exclusive" in the oclif
// flag definitions above, so oclif will enforce that they are not both used together.
if (!options.device && !options.application) {

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -61,12 +61,7 @@ export default class OsInitializeCmd extends Command {
public static usage = 'os initialize <image>';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
type: cf.deviceType,
drive: cf.drive,
yes: cf.yes,
help: cf.help,
@ -79,9 +74,9 @@ export default class OsInitializeCmd extends Command {
OsInitializeCmd,
);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { getManifest, sudo } = await import('../../utils/helpers');
const { getManifest, safeUmount, sudo } = await import(
'../../utils/helpers'
);
console.info(`Initializing device ${INIT_WARNING_MESSAGE}`);
@ -101,7 +96,7 @@ export default class OsInitializeCmd extends Command {
`Going to erase ${answers.drive}.`,
true,
);
await umountAsync(answers.drive);
await safeUmount(answers.drive);
}
await sudo([
@ -113,22 +108,7 @@ export default class OsInitializeCmd extends Command {
]);
if (answers.drive != null) {
// TODO: balena local makes use of ejectAsync, see below
// DO we need this / should we do that here?
// getDrive = (drive) ->
// driveListAsync().then (drives) ->
// selectedDrive = _.find(drives, device: drive)
// if not selectedDrive?
// throw new Error("Drive not found: #{drive}")
// return selectedDrive
// if (os.platform() is 'win32') and selectedDrive.mountpoint?
// ejectAsync = Promise.promisify(require('removedrive').eject)
// return ejectAsync(selectedDrive.mountpoint)
await umountAsync(answers.drive);
await safeUmount(answers.drive);
console.info(`You can safely remove ${answers.drive} now`);
}
}

View File

@ -17,12 +17,14 @@
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import {
getBalenaSdk,
getCliForm,
getVisuals,
stripIndent,
} from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import type { DockerConnectionCliFlags } from '../utils/docker';
import { dockerConnectionCliFlags } from '../utils/docker';
import * as _ from 'lodash';
@ -43,6 +45,7 @@ interface FlagsDef extends DockerConnectionCliFlags {
'splash-image'?: string;
'dont-check-arch': boolean;
'pin-device-to-release': boolean;
'additional-space'?: number;
'add-certificate'?: string[];
help: void;
}
@ -60,15 +63,27 @@ export default class PreloadCmd extends Command {
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.
preloaded. This is usually combined with release pinning
(https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/)
to avoid the device dowloading a newer release straight away, if one is available.
Check also the Preloading and Preregistering section of the balena CLI's advanced
masterclass document:
https://www.balena.io/docs/learn/more/masterclasses/advanced-cli/#5-preloading-and-preregistering
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
${applicationIdInfo.split('\n').join('\n\t\t')}
Note that the this command requires Docker to be installed, as further detailed
in the balena CLI's installation instructions:
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
The \`--dockerHost\` and \`--dockerPort\` flags allow a remote Docker engine to
be used, however the image file must be accessible to the remote Docker engine
on the same path given on the command line. This is because Docker's bind mount
feature is used to "share" the image with a container that performs the preload.
`;
public static examples = [
'$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png',
'$ balena preload balena.img --app MyApp --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0',
'$ balena preload balena.img --app myorg/myapp --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png',
'$ balena preload balena.img',
];
@ -83,10 +98,8 @@ export default class PreloadCmd extends Command {
public static usage = 'preload <image>';
public static flags: flags.Input<FlagsDef> = {
app: flags.string({
description: 'name of the application to preload',
char: 'a',
}),
// TODO: Replace with application/a in #v13?
app: cf.application,
commit: flags.string({
description: `\
The commit hash for a specific application release to preload, use "current" to specify the current
@ -100,14 +113,21 @@ manually pinned using https://github.com/balena-io-projects/staged-releases .\
char: 's',
}),
'dont-check-arch': flags.boolean({
default: false,
description:
'disables check for matching architecture in image and application',
}),
'pin-device-to-release': flags.boolean({
default: false,
description:
'pin the preloaded device to the preloaded release on provision',
char: 'p',
}),
'additional-space': flags.integer({
description:
'expand the image by this amount of bytes instead of automatically estimating the required amount',
parse: (x) => parseAsInteger(x, 'additional-space'),
}),
'add-certificate': flags.string({
description: `\
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
@ -155,6 +175,18 @@ Can be repeated to add multiple certificates.\
);
}
// balena-preload currently does not work with numerical app IDs
// Load app here, and use app slug from hereon
if (options.app && !options.app.includes('/')) {
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../utils/sdk');
const application = await getApplication(balena, options.app);
if (!application) {
throw new ExpectedError(`Application not found: ${options.app}`);
}
options.app = application.slug;
}
const progressBars: {
[key: string]: ReturnType<typeof getVisuals>['Progress'];
} = {};
@ -192,13 +224,14 @@ Can be repeated to add multiple certificates.\
const appId = options.app;
const splashImage = options['splash-image'];
const additionalSpace = options['additional-space'];
const dontCheckArch = options['dont-check-arch'] || false;
const pinDevice = options['pin-device-to-release'] || false;
if (dontCheckArch && !appId) {
throw new ExpectedError(
'You need to specify an app id if you disable the architecture check.',
'You need to specify an application if you disable the architecture check.',
);
}
@ -223,6 +256,7 @@ Can be repeated to add multiple certificates.\
dontCheckArch,
pinDevice,
certificates,
additionalSpace,
);
let gotSignal = false;

View File

@ -20,44 +20,52 @@ import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import type { BalenaSDK, Application, Organization } from 'balena-sdk';
import type { BalenaSDK } from 'balena-sdk';
import { ExpectedError, instanceOf } from '../errors';
import { isV13 } from '../utils/version';
import { RegistrySecrets } from 'resin-multibuild';
import { lowercaseIfSlug } from '../utils/normalization';
import {
applyReleaseTagKeysAndValues,
parseReleaseTagKeysAndValues,
} from '../utils/compose_ts';
enum BuildTarget {
Cloud,
Device,
}
interface FlagsDef {
source?: string;
emulated: boolean;
dockerfile?: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
nocache?: boolean;
pull?: boolean;
'noparent-check'?: boolean;
'registry-secrets'?: string;
gitignore?: boolean;
nogitignore?: boolean;
nolive?: boolean;
detached?: boolean;
service?: string[];
system?: boolean;
env?: string[];
'convert-eol'?: boolean;
'noconvert-eol'?: boolean;
'multi-dockerignore'?: boolean;
help: void;
}
interface ArgsDef {
applicationOrDevice: string;
}
interface FlagsDef {
source: string;
emulated: boolean;
dockerfile?: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
nocache: boolean;
pull: boolean;
'noparent-check': boolean;
'registry-secrets'?: string;
gitignore?: boolean;
nogitignore?: boolean;
nolive: boolean;
detached: boolean;
service?: string[];
system: boolean;
env?: string[];
'convert-eol'?: boolean;
'noconvert-eol': boolean;
'multi-dockerignore': boolean;
'release-tag'?: string[];
help: void;
}
export default class PushCmd extends Command {
public static description = stripIndent`
Start a remote build on the balenaCloud build servers or a local mode device.
Start a build on the remote balenaCloud build servers, or a local mode device.
Start a build on the remote balenaCloud builders, or a local mode balena device.
Start a build on the remote balenaCloud build servers, or a local mode device.
When building on the balenaCloud servers, the given source directory will be
sent to the remote server. This can be used as a drop-in replacement for the
@ -92,6 +100,8 @@ export default class PushCmd extends Command {
'$ balena push myApp',
'$ balena push myApp --source <source directory>',
'$ balena push myApp -s <source directory>',
'$ balena push myApp --release-tag key1 "" key2 "value2 with spaces"',
'$ balena push myorg/myapp',
'',
'$ balena push 10.0.0.1',
'$ balena push 10.0.0.1 --source <source directory>',
@ -106,8 +116,10 @@ export default class PushCmd extends Command {
public static args = [
{
name: 'applicationOrDevice',
description: 'application name, or device address (for local pushes)',
description:
'application name or slug, or local device IP address or hostname',
required: true,
parse: lowercaseIfSlug,
},
];
@ -119,12 +131,15 @@ export default class PushCmd extends Command {
Source directory to be sent to balenaCloud or balenaOS device
(default: current working dir)`,
char: 's',
default: '.',
}),
emulated: flags.boolean({
description: stripIndent`
Don't use native ARM servers; force QEMU ARM emulation on Intel x86-64
servers during the image build (balenaCloud).`,
Don't use the faster, native balenaCloud ARM builders; force slower QEMU ARM
emulation on Intel x86-64 builders. This flag is sometimes used to investigate
suspected issues with the balenaCloud backend.`,
char: 'e',
default: false,
}),
dockerfile: flags.string({
description:
@ -139,15 +154,18 @@ export default class PushCmd extends Command {
updates), but the logs will not display the "Using cache" lines for each
build step of a Dockerfile.`,
char: 'c',
default: false,
}),
pull: flags.boolean({
description: stripIndent`
When pushing to a local device, force the base images to be pulled again.
Currently this option is ignored when pushing to the balenaCloud builders.`,
default: false,
}),
'noparent-check': flags.boolean({
description: stripIndent`
Disable project validation check of 'docker-compose.yml' file in parent folder`,
default: false,
}),
'registry-secrets': flags.string({
description: stripIndent`
@ -163,6 +181,7 @@ export default class PushCmd extends Command {
and changes will not be synchronized to any running containers. Note that both
this flag and --detached and required to cause the process to end once the
initial build has completed.`,
default: false,
}),
detached: flags.boolean({
description: stripIndent`
@ -171,6 +190,7 @@ export default class PushCmd extends Command {
applicable). When pushing to a local mode device, this option will cause
the command to not tail application logs when the build has completed.`,
char: 'd',
default: false,
}),
service: flags.string({
description: stripIndent`
@ -183,6 +203,7 @@ export default class PushCmd extends Command {
description: stripIndent`
Only show system logs. This can be used in combination with --service.
Only valid when pushing to a local mode device.`,
default: false,
}),
env: flags.string({
description: stripIndent`
@ -196,34 +217,56 @@ export default class PushCmd extends Command {
`,
multiple: true,
}),
'convert-eol': flags.boolean({
description: 'No-op and deprecated since balena CLI v12.0.0',
char: 'l',
hidden: true,
}),
...(isV13()
? {}
: {
'convert-eol': flags.boolean({
description: 'No-op and deprecated since balena CLI v12.0.0',
char: 'l',
hidden: true,
default: false,
}),
}),
'noconvert-eol': flags.boolean({
description: `Don't convert line endings from CRLF (Windows format) to LF (Unix format).`,
default: false,
}),
'multi-dockerignore': flags.boolean({
description:
'Have each service use its own .dockerignore file. See "balena help push".',
char: 'm',
default: false,
exclusive: ['gitignore'],
}),
nogitignore: flags.boolean({
description:
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
char: 'G',
hidden: true,
}),
...(isV13()
? {}
: {
nogitignore: flags.boolean({
description:
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
char: 'G',
hidden: true,
default: false,
}),
}),
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.`,
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.`,
char: 'g',
default: false,
exclusive: ['multi-dockerignore'],
}),
'release-tag': flags.string({
description: stripIndent`
Set release tags if the push to a cloud application is successful. Multiple
arguments may be provided, alternating tag keys and values (see examples).
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
`,
multiple: true,
exclusive: ['detached'],
}),
help: cf.help,
};
@ -234,186 +277,181 @@ export default class PushCmd extends Command {
PushCmd,
);
const logger = await Command.getLogger();
logger.logDebug(`Using build source directory: ${options.source} `);
const sdk = getBalenaSdk();
const { validateProjectDirectory } = await import('../utils/compose_ts');
const source = options.source || '.';
if (process.env.DEBUG) {
console.error(`[debug] Using ${source} as build source`);
}
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
sdk,
{
dockerfilePath: options.dockerfile,
noParentCheck: options['noparent-check'] || false,
projectPath: source,
noParentCheck: options['noparent-check'],
projectPath: options.source,
registrySecretsPath: options['registry-secrets'],
},
);
const nogitignore = !options.gitignore;
const convertEol = !options['noconvert-eol'];
const appOrDevice = params.applicationOrDevice;
const buildTarget = await this.getBuildTarget(appOrDevice);
switch (buildTarget) {
switch (await this.getBuildTarget(params.applicationOrDevice)) {
case BuildTarget.Cloud:
const remote = await import('../utils/remote-build');
logger.logDebug(
`Pushing to cloud for application: ${params.applicationOrDevice}`,
);
// Check for invalid options
const localOnlyOptions = ['nolive', 'service', 'system', 'env'];
localOnlyOptions.forEach((opt) => {
// @ts-ignore : Not sure why typescript wont let me do this?
if (options[opt]) {
throw new ExpectedError(
`The --${opt} flag is only valid when pushing to a local mode device`,
);
}
});
const app = appOrDevice;
await Command.checkLoggedIn();
const [token, baseUrl, owner] = await Promise.all([
sdk.auth.getToken(),
sdk.settings.get('balenaUrl'),
this.getAppOwner(sdk, app),
]);
const opts = {
dockerfilePath,
emulated: options.emulated || false,
multiDockerignore: options['multi-dockerignore'] || false,
nocache: options.nocache || false,
registrySecrets,
headless: options.detached || false,
convertEol,
};
const args = {
app,
owner,
source,
auth: token,
baseUrl,
nogitignore,
await this.pushToCloud(
params.applicationOrDevice,
options,
sdk,
opts,
};
await remote.startRemoteBuild(args);
dockerfilePath,
registrySecrets,
);
break;
case BuildTarget.Device:
const deviceDeploy = await import('../utils/device/deploy');
const device = appOrDevice;
const servicesToDisplay = options.service;
// TODO: Support passing a different port
try {
await deviceDeploy.deployToDevice({
source,
deviceHost: device,
dockerfilePath,
registrySecrets,
multiDockerignore: options['multi-dockerignore'] || false,
nocache: options.nocache || false,
pull: options.pull || false,
nogitignore,
noParentCheck: options['noparent-check'] || false,
nolive: options.nolive || false,
detached: options.detached || false,
services: servicesToDisplay,
system: options.system || false,
env: options.env || [],
convertEol,
});
} catch (e) {
const { BuildError } = await import('../utils/device/errors');
if (instanceOf(e, BuildError)) {
throw new ExpectedError(e.toString());
} else {
throw e;
}
}
logger.logDebug(
`Pushing to local device: ${params.applicationOrDevice}`,
);
await this.pushToDevice(
params.applicationOrDevice,
options,
dockerfilePath,
registrySecrets,
);
break;
default:
throw new ExpectedError(stripIndent`
Build target not recognized. Please provide either an application name or
device IP address.`);
}
}
async getBuildTarget(appOrDevice: string): Promise<BuildTarget | null> {
const {
validateApplicationName,
validateDotLocalUrl,
validateIPAddress,
} = await import('../utils/validation');
protected async pushToCloud(
appNameOrSlug: string,
options: FlagsDef,
sdk: BalenaSDK,
dockerfilePath: string,
registrySecrets: RegistrySecrets,
) {
const remote = await import('../utils/remote-build');
const { getApplication } = await import('../utils/sdk');
// First try the application regex from the api
if (validateApplicationName(appOrDevice)) {
return BuildTarget.Cloud;
}
if (validateIPAddress(appOrDevice) || validateDotLocalUrl(appOrDevice)) {
return BuildTarget.Device;
}
return null;
}
async getAppOwner(sdk: BalenaSDK, appName: string) {
const _ = await import('lodash');
const applications = (await sdk.models.application.getAll({
$expand: {
organization: {
$select: ['handle'],
},
},
$filter: {
$eq: [{ $tolower: { $: 'app_name' } }, appName.toLowerCase()],
},
$select: ['id'],
})) as Array<
Application & {
organization: [Organization];
}
>;
if (applications == null || applications.length === 0) {
throw new ExpectedError(
stripIndent`
No applications found with name: ${appName}.
This could mean that the application does not exist, or you do
not have the permissions required to access it.`,
);
}
if (applications.length === 1) {
return applications[0].organization[0].handle;
}
// If we got more than one application with the same name it means that the
// 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 username = app.organization[0].handle;
return {
name: `${username}/${appName}`,
extra: username,
};
});
const { selectFromList } = await import('../utils/patterns');
const selected = await selectFromList(
`${entries.length} applications found with that name, please select the application you would like to push to`,
entries,
// Check for invalid options
const localOnlyOptions: Array<keyof FlagsDef> = [
'nolive',
'service',
'system',
'env',
];
this.checkInvalidOptions(
localOnlyOptions,
options,
'is only valid when pushing to a local mode device',
);
return selected.extra;
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
options['release-tag'] ?? [],
);
await Command.checkLoggedIn();
const [token, baseUrl] = await Promise.all([
sdk.auth.getToken(),
sdk.settings.get('balenaUrl'),
]);
const application = await getApplication(sdk, appNameOrSlug, {
$select: ['app_name', 'slug'],
});
const opts = {
dockerfilePath,
emulated: options.emulated,
multiDockerignore: options['multi-dockerignore'],
nocache: options.nocache,
registrySecrets,
headless: options.detached,
convertEol: !options['noconvert-eol'],
};
const args = {
appSlug: application.slug,
source: options.source,
auth: token,
baseUrl,
nogitignore: !options.gitignore,
sdk,
opts,
};
const releaseId = await remote.startRemoteBuild(args);
if (releaseId) {
await applyReleaseTagKeysAndValues(
sdk,
releaseId,
releaseTagKeys,
releaseTagValues,
);
} else if (releaseTagKeys.length > 0) {
throw new Error(stripIndent`
A release ID could not be parsed out of the builder's output.
As a result, the release tags have not been set.`);
}
}
protected async pushToDevice(
localDeviceAddress: string,
options: FlagsDef,
dockerfilePath: string,
registrySecrets: RegistrySecrets,
) {
// Check for invalid options
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag'];
this.checkInvalidOptions(
remoteOnlyOptions,
options,
'is only valid when pushing to an application',
);
const deviceDeploy = await import('../utils/device/deploy');
try {
await deviceDeploy.deployToDevice({
source: options.source,
deviceHost: localDeviceAddress,
dockerfilePath,
registrySecrets,
multiDockerignore: options['multi-dockerignore'],
nocache: options.nocache,
pull: options.pull,
nogitignore: !options.gitignore,
noParentCheck: options['noparent-check'],
nolive: options.nolive,
detached: options.detached,
services: options.service,
system: options.system,
env: options.env || [],
convertEol: !options['noconvert-eol'],
});
} catch (e) {
const { BuildError } = await import('../utils/device/errors');
if (instanceOf(e, BuildError)) {
throw new ExpectedError(e.toString());
} else {
throw e;
}
}
}
protected async getBuildTarget(appOrDevice: string): Promise<BuildTarget> {
const { validateLocalHostnameOrIp } = await import('../utils/validation');
return validateLocalHostnameOrIp(appOrDevice)
? BuildTarget.Device
: BuildTarget.Cloud;
}
protected checkInvalidOptions(
invalidOptions: Array<keyof FlagsDef>,
options: FlagsDef,
errorMessage: string,
) {
invalidOptions.forEach((opt) => {
if (options[opt]) {
throw new ExpectedError(`The --${opt} flag ${errorMessage}`);
}
});
}
}

View File

@ -50,8 +50,8 @@ export default class ScanCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
verbose: flags.boolean({
char: 'v',
default: false,
char: 'v',
description: 'display full info',
}),
timeout: flags.integer({
@ -60,6 +60,7 @@ export default class ScanCmd extends Command {
}),
help: cf.help,
json: flags.boolean({
default: false,
char: 'j',
description: 'produce JSON output instead of tabular output',
}),

View File

@ -19,11 +19,7 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import {
parseAsInteger,
validateDotLocalUrl,
validateIPAddress,
} from '../utils/validation';
import { parseAsInteger, validateLocalHostnameOrIp } from '../utils/validation';
import * as BalenaSdk from 'balena-sdk';
interface FlagsDef {
@ -39,14 +35,14 @@ interface ArgsDef {
service?: string;
}
export default class NoteCmd extends Command {
export default class SshCmd extends Command {
public static description = stripIndent`
SSH into the host or application container of a device.
Start a shell on a local or remote device. If a service name is not provided,
a shell will be opened on the host OS.
If an application name is provided, an interactive menu will be presented
If an application is provided, an interactive menu will be presented
for the selection of an online device. A shell will then be opened for the
host OS or service container of the chosen device.
@ -81,7 +77,8 @@ export default class NoteCmd extends Command {
public static args = [
{
name: 'applicationOrDevice',
description: 'application name, device uuid, or address of local device',
description:
'application name/slug/id, device uuid, or address of local device',
required: true,
},
{
@ -102,16 +99,19 @@ export default class NoteCmd extends Command {
parse: (p) => parseAsInteger(p, 'port'),
}),
tty: flags.boolean({
default: false,
description:
'Force pseudo-terminal allocation (bypass TTY autodetection for stdin)',
'force pseudo-terminal allocation (bypass TTY autodetection for stdin)',
char: 't',
}),
verbose: flags.boolean({
description: 'Increase verbosity',
default: false,
description: 'increase verbosity',
char: 'v',
}),
noproxy: flags.boolean({
description: 'Bypass global proxy configuration for the ssh connection',
default: false,
description: 'bypass global proxy configuration for the ssh connection',
}),
help: cf.help,
};
@ -120,14 +120,11 @@ export default class NoteCmd extends Command {
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
NoteCmd,
SshCmd,
);
// if we're doing a direct SSH connection locally...
if (
validateDotLocalUrl(params.applicationOrDevice) ||
validateIPAddress(params.applicationOrDevice)
) {
// Local connection
if (validateLocalHostnameOrIp(params.applicationOrDevice)) {
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
return await performLocalDeviceSSH({
address: params.applicationOrDevice,
@ -138,26 +135,27 @@ export default class NoteCmd extends Command {
});
}
// Remote connection
const { getProxyConfig, which } = await import('../utils/helpers');
const { checkLoggedIn, getOnlineTargetUuid } = await import(
'../utils/patterns'
);
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
const sdk = getBalenaSdk();
const proxyConfig = getProxyConfig();
const useProxy = !!proxyConfig && !options.noproxy;
// this will be a tunnelled SSH connection...
await checkLoggedIn();
const uuid = await getOnlineTargetUuid(sdk, params.applicationOrDevice);
let version: string | undefined;
let id: number | undefined;
await Command.checkLoggedIn();
const deviceUuid = await getOnlineTargetDeviceUuid(
sdk,
params.applicationOrDevice,
);
const device = await sdk.models.device.get(uuid, {
const device = await sdk.models.device.get(deviceUuid, {
$select: ['id', 'supervisor_version', 'is_online'],
});
id = device.id;
version = device.supervisor_version;
const deviceId = device.id;
const supervisorVersion = device.supervisor_version;
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
useProxy ? which('proxytunnel', false) : undefined,
@ -204,20 +202,13 @@ export default class NoteCmd extends Command {
const proxyCommand = useProxy ? getSshProxyCommand() : undefined;
if (username == null) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
`Opening an SSH connection to a remote device requires you to be logged in.`,
);
}
// At this point, we have a long uuid with a device
// At this point, we have a long uuid of a device
// that we know exists and is accessible
let containerId: string | undefined;
if (params.service != null) {
containerId = await this.getContainerId(
sdk,
uuid,
deviceUuid,
params.service,
{
port: options.port,
@ -225,20 +216,20 @@ export default class NoteCmd extends Command {
proxyUrl: proxyUrl || '',
username: username!,
},
version,
id,
supervisorVersion,
deviceId,
);
}
let accessCommand: string;
if (containerId != null) {
accessCommand = `enter ${uuid} ${containerId}`;
accessCommand = `enter ${deviceUuid} ${containerId}`;
} else {
accessCommand = `host ${uuid}`;
accessCommand = `host ${deviceUuid}`;
}
const command = this.generateVpnSshCommand({
uuid,
uuid: deviceUuid,
command: accessCommand,
verbose: options.verbose,
port: options.port,

View File

@ -20,6 +20,7 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
interface FlagsDef {
application?: string;
@ -45,12 +46,14 @@ export default class SupportCmd extends Command {
Both --device and --application flags accept multiple values, specified as
a comma-separated list (with no spaces).
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'balena support enable --device ab346f,cd457a --duration 3d',
'balena support enable --application app3 --duration 12h',
'balena support disable -a myApp',
'balena support disable -a myorg/myapp',
];
public static args = [
@ -68,10 +71,11 @@ export default class SupportCmd extends Command {
description: 'comma-separated list (no spaces) of device UUIDs',
char: 'd',
}),
application: flags.string({
description: 'comma-separated list (no spaces) of application names',
char: 'a',
}),
application: {
...cf.application,
description:
'comma-separated list (no spaces) of application names or org/name slugs',
},
duration: flags.string({
description:
'length of time to enable support for, in (h)ours or (d)ays, e.g. 12h, 2d',

View File

@ -17,11 +17,9 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { disambiguateReleaseParam } from '../../utils/normalization';
import { tryAsInteger } from '../../utils/validation';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
application?: string;
@ -40,10 +38,13 @@ export default class TagRmCmd extends Command {
Remove a tag from an application, device or release.
Remove a tag from an application, device or release.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena tag rm myTagKey --application MyApp',
'$ balena tag rm myTagKey -a myorg/myapp',
'$ balena tag rm myTagKey --device 7cf02a6',
'$ balena tag rm myTagKey --release 1234',
'$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6',
@ -64,6 +65,10 @@ export default class TagRmCmd extends Command {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
@ -73,10 +78,6 @@ export default class TagRmCmd extends Command {
exclusive: ['app', 'application', 'device'],
},
help: cf.help,
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device', 'release'],
}),
};
public static authenticated = true;
@ -94,12 +95,16 @@ export default class TagRmCmd extends Command {
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(TagRmCmd.missingResourceMessage);
}
const { tryAsInteger } = await import('../../utils/validation');
if (options.application) {
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
return balena.models.application.tags.remove(
tryAsInteger(options.application),
await getTypedApplicationIdentifier(balena, options.application),
params.tagKey,
);
}
@ -110,6 +115,9 @@ export default class TagRmCmd extends Command {
);
}
if (options.release) {
const { disambiguateReleaseParam } = await import(
'../../utils/normalization'
);
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
@ -122,7 +130,7 @@ export default class TagRmCmd extends Command {
protected static missingResourceMessage = stripIndent`
To remove a resource tag, you must provide exactly one of:
* An application, with --application <appname>
* An application, with --application <appNameOrSlug>
* A device, with --device <uuid>
* A release, with --release <id or commit>

View File

@ -17,11 +17,9 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { disambiguateReleaseParam } from '../../utils/normalization';
import { tryAsInteger } from '../../utils/validation';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
application?: string;
@ -45,10 +43,13 @@ export default class TagSetCmd extends Command {
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.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena tag set mySimpleTag --application MyApp',
'$ balena tag set mySimpleTag -a myorg/myapp',
'$ balena tag set myCompositeTag myTagValue --application MyApp',
'$ balena tag set myCompositeTag myTagValue --device 7cf02a6',
'$ balena tag set myCompositeTag "my tag value with whitespaces" --device 7cf02a6',
@ -77,6 +78,10 @@ export default class TagSetCmd extends Command {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
@ -86,10 +91,6 @@ export default class TagSetCmd extends Command {
exclusive: ['app', 'application', 'device'],
},
help: cf.help,
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device', 'release'],
}),
};
public static authenticated = true;
@ -107,14 +108,18 @@ export default class TagSetCmd extends Command {
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(TagSetCmd.missingResourceMessage);
}
params.value ??= '';
const { tryAsInteger } = await import('../../utils/validation');
if (options.application) {
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
return balena.models.application.tags.set(
tryAsInteger(options.application),
await getTypedApplicationIdentifier(balena, options.application),
params.tagKey,
params.value,
);
@ -127,6 +132,9 @@ export default class TagSetCmd extends Command {
);
}
if (options.release) {
const { disambiguateReleaseParam } = await import(
'../../utils/normalization'
);
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
@ -143,7 +151,7 @@ export default class TagSetCmd extends Command {
protected static missingResourceMessage = stripIndent`
To set a resource tag, you must provide exactly one of:
* An application, with --application <appname>
* An application, with --application <appNameOrSlug>
* A device, with --device <uuid>
* A release, with --release <id or commit>

View File

@ -20,9 +20,7 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { disambiguateReleaseParam } from '../utils/normalization';
import { tryAsInteger } from '../utils/validation';
import { isV12 } from '../utils/version';
import { applicationIdInfo } from '../utils/messages';
interface FlagsDef {
application?: string;
@ -38,10 +36,13 @@ export default class TagsCmd extends Command {
List all tags and their values for a particular application,
device or release.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena tags --application MyApp',
'$ balena tags -a myorg/myapp',
'$ balena tags --device 7cf02a6',
'$ balena tags --release 1234',
'$ balena tags --release b376b0e544e9429483b656490e5b9443b4349bd6',
@ -54,6 +55,10 @@ export default class TagsCmd extends Command {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
@ -63,10 +68,6 @@ export default class TagsCmd extends Command {
exclusive: ['app', 'application', 'device'],
},
help: cf.help,
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device', 'release'],
}),
};
public static authenticated = true;
@ -85,11 +86,14 @@ export default class TagsCmd extends Command {
throw new ExpectedError(this.missingResourceMessage);
}
const { tryAsInteger } = await import('../utils/validation');
let tags;
if (options.application) {
const { getTypedApplicationIdentifier } = await import('../utils/sdk');
tags = await balena.models.application.tags.getAllByApplication(
tryAsInteger(options.application),
await getTypedApplicationIdentifier(balena, options.application),
);
}
if (options.device) {
@ -98,6 +102,9 @@ export default class TagsCmd extends Command {
);
}
if (options.release) {
const { disambiguateReleaseParam } = await import(
'../utils/normalization'
);
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
@ -110,17 +117,13 @@ export default class TagsCmd extends Command {
throw new ExpectedError('No tags found');
}
console.log(
isV12()
? getVisuals().table.horizontal(tags, ['tag_key', 'value'])
: getVisuals().table.horizontal(tags, ['id', 'tag_key', 'value']),
);
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
}
protected missingResourceMessage = stripIndent`
To list tags for a resource, you must provide exactly one of:
* An application, with --application <appname>
* An application, with --application <appNameOrSlug>
* A device, with --device <uuid>
* A release, with --release <id or commit>

View File

@ -24,11 +24,7 @@ import {
} from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { getOnlineTargetUuid } from '../utils/patterns';
import * as _ from 'lodash';
import { tunnelConnectionToDevice } from '../utils/tunnel';
import { createServer, Server, Socket } from 'net';
import { IArg } from '@oclif/parser/lib/args';
import type { Server, Socket } from 'net';
interface FlagsDef {
port: string[];
@ -43,17 +39,25 @@ export default class TunnelCmd extends Command {
public static description = stripIndent`
Tunnel local ports to your balenaOS device.
Use this command to open local ports which tunnel to listening ports on your balenaOS device.
Use this command to open local TCP ports that tunnel to listening sockets in a
balenaOS device.
For example, you could open port 8080 on your local machine to connect to your managed balenaOS
device running a web server listening on port 3000.
For example, this command could be used to expose the ssh server of a balenaOS
device (port number 22222) on the local machine, or to expose a web server
running on the device. The port numbers do not have be the same between the
device and the local machine, and multiple ports may be tunneled in a single
command line.
Port mappings are specified in the format: <remotePort>[:[localIP:]localPort]
localIP defaults to 'localhost', and localPort defaults to the specified remotePort value.
localIP defaults to 'localhost', and localPort defaults to the specified
remotePort value.
You can tunnel multiple ports at any given time.
Note: the -p (--port) flag must be provided at the end of the command line,
as per examples.
Note: Port mappings must come after the deviceOrApplication parameter, as per examples.
In the case of openBalena, the tunnel command in CLI v12.38.5 or later requires
openBalena v3.1.2 or later. Older CLI versions work with older openBalena
versions.
`;
public static examples = [
@ -73,10 +77,10 @@ export default class TunnelCmd extends Command {
'$ balena tunnel myApp -p 8080:3000 -p 8081:9000',
];
public static args: Array<IArg<any>> = [
public static args = [
{
name: 'deviceOrApplication',
description: 'device uuid or application name/id',
description: 'device uuid or application name/slug/id',
required: true,
},
];
@ -101,8 +105,7 @@ export default class TunnelCmd extends Command {
TunnelCmd,
);
const Logger = await import('../utils/logger');
const logger = Logger.getLogger();
const logger = await Command.getLogger();
const sdk = getBalenaSdk();
const logConnection = (
@ -127,23 +130,30 @@ export default class TunnelCmd extends Command {
throw new NoPortsDefinedError();
}
const uuid = await getOnlineTargetUuid(sdk, params.deviceOrApplication);
// Ascertain device uuid
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
const uuid = await getOnlineTargetDeviceUuid(
sdk,
params.deviceOrApplication,
);
const device = await sdk.models.device.get(uuid);
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
const _ = await import('lodash');
const localListeners = _.chain(options.port)
.map((mapping) => {
return this.parsePortMapping(mapping);
})
.map(async ({ localPort, localAddress, remotePort }) => {
try {
const { tunnelConnectionToDevice } = await import('../utils/tunnel');
const handler = await tunnelConnectionToDevice(
device.uuid,
remotePort,
sdk,
);
const { createServer } = await import('net');
const server = createServer(async (client: Socket) => {
try {
await handler(client);

View File

@ -43,7 +43,9 @@ export default class UtilAvailableDrivesCmd extends Command {
const sdk = await import('etcher-sdk');
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter({
includeSystemDrives: () => false,
});
const scanner = new sdk.scanner.Scanner([adapter]);
await scanner.start();

View File

@ -20,8 +20,8 @@ import Command from '../command';
import { stripIndent } from '../utils/lazy';
interface FlagsDef {
all?: boolean;
json?: boolean;
all: boolean;
json: boolean;
help: void;
}
@ -59,14 +59,14 @@ export default class VersionCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
all: flags.boolean({
char: 'a',
default: false,
char: 'a',
description:
'include version information for additional components (Node.js)',
}),
json: flags.boolean({
char: 'j',
default: false,
char: 'j',
description:
'output version information in JSON format for programmatic use',
}),

View File

@ -20,8 +20,12 @@ import * as os from 'os';
import { TypedError } from 'typed-error';
import { getChalk, stripIndent } from './utils/lazy';
import { getHelp } from './utils/messages';
import { CliSettings } from './utils/bootstrap';
export class ExpectedError extends TypedError {}
export class ExpectedError extends TypedError {
public code?: string;
public exitCode?: number;
}
export class NotLoggedInError extends ExpectedError {}
@ -39,6 +43,8 @@ export class NoPortsDefinedError extends ExpectedError {
}
}
export class SIGINTError extends ExpectedError {}
/**
* instanceOf is a more reliable implementation of the plain `instanceof`
* typescript operator, for use with TypedError errors when the error
@ -99,6 +105,17 @@ function interpret(error: Error): string {
return error.message;
}
function loadDataDirectory(): string {
try {
const settings = new CliSettings();
return settings.get('dataDirectory') as string;
} catch {
return os.platform() === 'win32'
? 'C:\\Users\\<user>\\_balena'
: '$HOME/.balena';
}
}
const messages: {
[key: string]: (error: Error & { path?: string }) => string;
} = {
@ -122,6 +139,23 @@ const messages: {
EACCES: (e) => messages.EPERM(e),
BalenaSettingsPermissionError: () => {
const dataDirectory = loadDataDirectory();
return stripIndent`
Error reading data directory: "${dataDirectory}"
This error usually indicates that the user doesn't have permissions over that directory,
which can happen if balena CLI was executed as the root user.
${
os.platform() === 'win32'
? `Try resetting the ownership by opening a new Command Prompt as administrator and running: \`takeown /f ${dataDirectory} /r\``
: `Try resetting the ownership by running: \`sudo chown -R $(whoami) ${dataDirectory}\``
}
`;
},
ETIMEDOUT: () =>
'Oops something went wrong, please check your connection and try again.',
@ -142,12 +176,21 @@ const messages: {
Try logging in again with the "balena login" command.`,
};
// TODO remove these regexes when we have a way of uniquely indentifying errors.
// related issue https://github.com/balena-io/balena-sdk/issues/1025
// related issue https://github.com/balena-io/balena-cli/issues/2126
const EXPECTED_ERROR_REGEXES = [
/cannot also be provided when using/, // Exclusive flag errors are all expected
/^BalenaSettingsPermissionError/, // balena-settings-storage
/^BalenaAmbiguousApplication/, // balena-sdk
/^BalenaAmbiguousDevice/, // balena-sdk
/^BalenaApplicationNotFound/, // balena-sdk
/^BalenaDeviceNotFound/, // balena-sdk
/^BalenaExpiredToken/, // balena-sdk
/^BalenaInvalidDeviceType/, // balena-sdk
/Cannot deactivate devices/i, // balena-api
/Devices must be offline in order to be deactivated\.$/i, // balena-api
/^BalenaOrganizationNotFound/, // balena-sdk
/Request error: Unauthorized$/, // balena-sdk
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError
/Missing required flag/, // oclif parser: RequiredFlagError
@ -171,12 +214,17 @@ async function sentryCaptureException(error: Error) {
await Sentry.close(1000);
} catch (e) {
if (process.env.DEBUG) {
console.error('Timeout reporting error to sentry.io');
console.error('[debug] Timeout reporting error to sentry.io');
}
}
}
export async function handleError(error: Error) {
export async function handleError(error: Error | string) {
// If a module has thrown a string, convert to error
if (typeof error === 'string') {
error = new Error(error);
}
// Set appropriate exitCode
process.exitCode =
(error as BalenaError).exitCode === 0
@ -206,8 +254,9 @@ export async function handleError(error: Error) {
if (!process.env.BALENARC_NO_SENTRY) {
await sentryCaptureException(error);
}
// Unhandled/unexpected error: ensure that the process terminates.
}
if (error instanceof SIGINTError || !isExpectedError) {
// SIGINT or unexpected error: ensure that the process terminates.
// The exit error code was set above through `process.exitCode`.
process.exit();
}
@ -241,6 +290,7 @@ export const printExpectedErrorMessage = function (message: string) {
* them and call this function.
*
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
* If a specific process exit code x must be set, use process.exitCode = x
*/
export function exitWithExpectedError(message: string | Error): never {
if (message instanceof Error) {

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2019 Balena Ltd.
* 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.
@ -14,26 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { lowercaseIfSlug } from './normalization';
declare module 'resin-image-fs' {
import Bluebird = require('bluebird');
export interface ImageDefinition {
image: string;
partition: number;
path: string;
}
export function readFile(options: {}): Bluebird<string>;
export function writeFile(
definition: ImageDefinition,
contents: string,
): Bluebird<void>;
export function copy(
input: ImageDefinition,
output: ImageDefinition,
): Bluebird<void>;
export function listDirectory(
definition: ImageDefinition,
): Bluebird<string[]>;
}
export const applicationRequired = {
name: 'application',
description: 'application name, slug (preferred), or numeric ID (deprecated)',
required: true,
parse: lowercaseIfSlug,
};

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2019 Balena Ltd.
* Copyright 2019-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,14 +19,17 @@ import { flags } from '@oclif/command';
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
import { stripIndent } from './lazy';
import { lowercaseIfSlug } from './normalization';
export const application = flags.string({
char: 'a',
description: 'application name',
description: 'application name, slug (preferred), or numeric ID (deprecated)',
parse: lowercaseIfSlug,
});
// TODO: Consider remove second alias 'app' when we can, to simplify.
export const app = flags.string({
description: "same as '--application'",
parse: lowercaseIfSlug,
});
export const device = flags.string({
@ -55,16 +58,19 @@ export const service = flags.string({
export const verbose: IBooleanFlag<boolean> = flags.boolean({
char: 'v',
description: 'produce verbose output',
default: false,
});
export const yes: IBooleanFlag<boolean> = flags.boolean({
char: 'y',
description: 'answer "yes" to all questions (non interactive use)',
default: false,
});
export const force: IBooleanFlag<boolean> = flags.boolean({
char: 'f',
description: 'force action if the update lock is set',
default: false,
});
export const drive = flags.string({
@ -75,3 +81,22 @@ export const drive = flags.string({
Check \`balena util available-drives\` for available options.
`,
});
export const driveOrImg = flags.string({
char: 'd',
description:
'path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)',
});
export const deviceType = flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
});
export const json: IBooleanFlag<boolean> = flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
default: false,
});

View File

@ -55,6 +55,7 @@ export interface ComposeOpts {
noParentCheck: boolean;
projectName: string;
projectPath: string;
isLocal?: boolean;
}
export interface ComposeCliFlags {
@ -81,7 +82,16 @@ export interface ComposeProject {
export interface Release {
client: ReturnType<typeof import('balena-release').createClient>;
release: Partial<import('balena-release/build/models').ReleaseModel>;
release: Pick<
import('balena-release/build/models').ReleaseModel,
| 'id'
| 'status'
| 'commit'
| 'composition'
| 'source'
| 'start_timestamp'
| 'end_timestamp'
>;
serviceImages: Partial<import('balena-release/build/models').ImageModel>;
}

View File

@ -56,7 +56,7 @@ export function createProject(composePath, composeStr, projectName = null) {
const compose = require('resin-compose-parse');
// both methods below may throw.
const rawComposition = yml.safeLoad(composeStr, {
const rawComposition = yml.load(composeStr, {
schema: yml.FAILSAFE_SCHEMA,
});
const composition = compose.normalize(rawComposition);
@ -200,11 +200,14 @@ export const createRelease = async function (
return {
client,
release: _.omit(release, [
'created_at',
'belongs_to__application',
'is_created_by__user',
'__metadata',
release: _.pick(release, [
'id',
'status',
'commit',
'composition',
'source',
'start_timestamp',
'end_timestamp',
]),
serviceImages: _.mapValues(serviceImages, (serviceImage) =>
_.omit(serviceImage, [
@ -366,15 +369,15 @@ export const pushAndUpdateServiceImages = function (
images.map(({ serviceImage, localImage, props, logs }, index) =>
Promise.all([
localImage.inspect().then((img) => img.Size),
retry(
retry({
// @ts-ignore
() => progress.push(localImage.name, reporters[index], opts),
3, // `times` - retry 3 times
func: () => progress.push(localImage.name, reporters[index], opts),
maxAttempts: 3, // try calling func 3 times (max)
// @ts-ignore
localImage.name, // `label` included in retry log messages
2000, // `delayMs` - wait 2 seconds before the 1st retry
1.4, // `backoffScaler` - wait multiplier for each retry
).finally(renderer.end),
label: localImage.name, // label for retry log messages
initialDelayMs: 2000, // wait 2 seconds before the 1st retry
backoffScaler: 1.4, // wait multiplier for each retry
}).finally(renderer.end),
])
.then(
/** @type {([number, string]) => void} */

View File

@ -44,6 +44,60 @@ import type { DeviceInfo } from './device/api';
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
import Logger = require('./logger');
/**
* Given an array representing the raw `--release-tag` flag of the deploy and
* push commands, parse it into separate arrays of release tag keys and values.
* The returned keys and values arrays are guaranteed to be of the same length.
*/
export function parseReleaseTagKeysAndValues(
releaseTags: string[],
): { releaseTagKeys: string[]; releaseTagValues: string[] } {
if (releaseTags.length === 0) {
return { releaseTagKeys: [], releaseTagValues: [] };
}
const releaseTagKeys = releaseTags.filter((_v, i) => i % 2 === 0);
const releaseTagValues = releaseTags.filter((_v, i) => i % 2 === 1);
releaseTagKeys.forEach((key: string) => {
if (key === '') {
throw new ExpectedError(`Error: --release-tag keys cannot be empty`);
}
if (/\s/.test(key)) {
throw new ExpectedError(
`Error: --release-tag keys cannot contain whitespaces`,
);
}
});
if (releaseTagKeys.length !== releaseTagValues.length) {
releaseTagValues.push('');
}
return { releaseTagKeys, releaseTagValues };
}
/**
* Use the balena SDK `models.release.tags.set()` method to set release tags
* for the given release ID. The releaseTagKeys and releaseTagValues arrays
* must be of the same length; their items map 1-to-1 to form key-value pairs.
*/
export async function applyReleaseTagKeysAndValues(
sdk: BalenaSDK,
releaseId: number,
releaseTagKeys: string[],
releaseTagValues: string[],
) {
if (releaseTagKeys.length === 0) {
return;
}
await Promise.all(
(_.zip(releaseTagKeys, releaseTagValues) as Array<[string, string]>).map(
async ([key, value]) => {
await sdk.models.release.tags.set(releaseId, key, value);
},
),
);
}
const exists = async (filename: string) => {
try {
await fs.access(filename);
@ -82,6 +136,7 @@ export async function loadProject(
} else {
logger.logDebug('Resolving project...');
[composeName, composeStr] = await resolveProject(logger, opts.projectPath);
if (composeName) {
if (opts.dockerfilePath) {
logger.logWarn(
@ -94,11 +149,57 @@ export async function loadProject(
);
composeStr = compose.defaultComposition(undefined, opts.dockerfilePath);
}
// If local push, merge dev compose overlay
if (opts.isLocal) {
composeStr = await mergeDevComposeOverlay(
logger,
composeStr,
opts.projectPath,
);
}
}
logger.logDebug('Creating project...');
return createProject(opts.projectPath, composeStr, opts.projectName);
}
/**
* Check for existence of docker-compose dev overlay file
* and merge in services definitions.
*/
async function mergeDevComposeOverlay(
logger: Logger,
composeStr: string,
projectRoot: string,
) {
const devOverlayFilename = 'docker-compose.dev.yml';
const devOverlayPath = path.join(projectRoot, devOverlayFilename);
if (await exists(devOverlayPath)) {
logger.logInfo(
`Docker compose dev overlay detected (${devOverlayFilename}) - merging.`,
);
interface ComposeObj {
services?: object;
}
const yaml = await import('js-yaml');
const loadObj = (inputStr: string): ComposeObj =>
(yaml.load(inputStr) || {}) as ComposeObj;
try {
const compose = loadObj(composeStr);
const devOverlay = loadObj(await fs.readFile(devOverlayPath, 'utf8'));
// We only want to merge the services section
compose.services = { ...compose.services, ...devOverlay.services };
composeStr = yaml.dump(compose, { styles: { '!!null': 'empty' } });
} catch (err) {
err.message = `Error merging docker compose dev overlay file "${devOverlayPath}":\n${err.message}`;
throw err;
}
}
return composeStr;
}
/**
* Look into the given directory for valid compose files and return
* the contents of the first one found.
@ -127,6 +228,7 @@ async function resolveProject(
if (!quiet && !composeFileName) {
logger.logInfo(`No "docker-compose.yml" file found at "${projectRoot}"`);
}
return [composeFileName, composeFileContents];
}
@ -532,7 +634,7 @@ async function loadBuildMetatada(
if (metadataPath.endsWith('json')) {
buildMetadata = JSON.parse(rawString);
} else {
buildMetadata = require('js-yaml').safeLoad(rawString);
buildMetadata = require('js-yaml').load(rawString);
}
} catch (err) {
throw new ExpectedError(
@ -875,7 +977,7 @@ async function parseRegistrySecrets(
const raw = (await fs.readFile(secretsFilename)).toString();
const multiBuild = await import('resin-multibuild');
const registrySecrets = new multiBuild.RegistrySecretValidator().validateRegistrySecrets(
isYaml ? require('js-yaml').safeLoad(raw) : JSON.parse(raw),
isYaml ? require('js-yaml').load(raw) : JSON.parse(raw),
);
multiBuild.addCanonicalDockerHubEntry(registrySecrets);
return registrySecrets;
@ -1128,15 +1230,10 @@ export async function validateProjectDirectory(
checkCompose(path.join(opts.projectPath, '..')),
]);
if (!hasCompose && hasParentCompose) {
const { isV12 } = await import('./version');
const msg = stripIndent`
"docker-compose.y[a]ml" file found in parent directory: please check that
the correct source folder was specified. (Suppress with '--noparent-check'.)`;
if (isV12()) {
throw new ExpectedError(`Error: ${msg}`);
} else {
Logger.getLogger().logWarn(msg);
}
throw new ExpectedError(`Error: ${msg}`);
}
}
}
@ -1204,7 +1301,7 @@ export async function deployProject(
auth: string,
apiEndpoint: string,
skipLogUpload: boolean,
): Promise<Partial<import('balena-release/build/models').ReleaseModel>> {
): Promise<import('balena-release/build/models').ReleaseModel> {
const releaseMod = await import('balena-release');
const { createRelease, tagServiceImages } = await import('./compose');
const tty = (await import('./tty'))(process.stdout);

View File

@ -28,6 +28,7 @@ import {
import type { Readable } from 'stream';
import { BALENA_ENGINE_TMP_PATH } from '../../config';
import { ExpectedError } from '../../errors';
import {
checkBuildSecretsRequirements,
loadProject,
@ -37,9 +38,9 @@ import {
import Logger = require('../logger');
import { DeviceAPI, DeviceInfo } from './api';
import * as LocalPushErrors from './errors';
import { DeviceAPIError } from './errors';
import LivepushManager from './live';
import { displayBuildLog } from './logs';
import { stripIndent } from '../lazy';
const LOCAL_APPNAME = 'localapp';
const LOCAL_RELEASEHASH = 'localrelease';
@ -76,7 +77,6 @@ async function environmentFromInput(
serviceNames: string[],
logger: Logger,
): Promise<ParsedEnvironment> {
const { exitWithExpectedError } = await import('../../errors');
// A normal environment variable regex, with an added part
// to find a colon followed servicename at the start
const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/;
@ -92,7 +92,7 @@ async function environmentFromInput(
for (const env of envs) {
const maybeMatch = env.match(varRegex);
if (maybeMatch == null) {
exitWithExpectedError(`Unable to parse environment variable: ${env}`);
throw new ExpectedError(`Unable to parse environment variable: ${env}`);
}
const match = maybeMatch!;
let service: string | undefined;
@ -122,9 +122,6 @@ async function environmentFromInput(
}
export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
const { exitWithExpectedError } = await import('../../errors');
const { displayDeviceLogs } = await import('./logs');
// Resolve .local addresses to IP to avoid
// issue with Windows and rapid repeat lookups.
// see: https://github.com/balena-io/balena-cli/issues/1518
@ -137,16 +134,19 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
opts.deviceHost = address;
}
const api = new DeviceAPI(globalLogger, opts.deviceHost);
const port = 48484;
const api = new DeviceAPI(globalLogger, opts.deviceHost, port);
// First check that we can access the device with a ping
try {
globalLogger.logDebug('Checking we can access device');
await api.ping();
} catch (e) {
exitWithExpectedError(
`Could not communicate with local mode device at address ${opts.deviceHost}`,
);
throw new ExpectedError(stripIndent`
Could not communicate with device supervisor at address ${opts.deviceHost}:${port}.
Device may not have local mode enabled. Check with:
balena device local-mode <device-uuid>
`);
}
const versionError = new Error(
@ -156,9 +156,9 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
try {
const version = await api.getVersion();
globalLogger.logDebug(`Checking device version: ${version}`);
globalLogger.logDebug(`Checking device supervisor version: ${version}`);
if (!semver.satisfies(version, '>=7.21.4')) {
exitWithExpectedError(versionError);
throw new ExpectedError(versionError);
}
if (!opts.nolive && !semver.satisfies(version, '>=9.7.0')) {
globalLogger.logWarn(
@ -169,8 +169,8 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
} catch (e) {
// Very old supervisor versions do not support /version endpoint
// a DeviceAPIError is expected in this case
if (e instanceof DeviceAPIError) {
exitWithExpectedError(versionError);
if (e instanceof LocalPushErrors.DeviceAPIError) {
throw new ExpectedError(versionError);
} else {
throw e;
}
@ -186,6 +186,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
noParentCheck: opts.noParentCheck,
projectName: 'local',
projectPath: opts.source,
isLocal: true,
});
// Attempt to attach to the device's docker daemon
@ -210,7 +211,10 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
if (!opts.nolive) {
buildLogs = {};
}
const buildTasks = await performBuilds(
const { awaitInterruptibleTask } = await import('../helpers');
const buildTasks = await awaitInterruptibleTask<typeof performBuilds>(
performBuilds,
project.composition,
tarStream,
docker,
@ -220,6 +224,10 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
buildLogs,
);
globalLogger.outputDeferredMessages();
// Print a newline to clearly separate build time and runtime
console.log();
const envs = await environmentFromInput(
opts.env,
Object.getOwnPropertyNames(project.composition.services),
@ -244,10 +252,11 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
// Now that we've set the target state, the device will do it's thing
// so we can either just display the logs, or start a livepush session
// (whilst also display logs)
const promises: Array<Promise<void>> = [streamDeviceLogs(api, opts)];
let livepush: LivepushManager | null = null;
if (!opts.nolive) {
// Print a newline to clear seperate build time and runtime
console.log();
const livepush = new LivepushManager({
livepush = new LivepushManager({
api,
buildContext: opts.source,
buildTasks,
@ -257,41 +266,40 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
buildLogs: buildLogs!,
deployOpts: opts,
});
const promises: Array<Promise<void>> = [livepush.init()];
// Only show logs if we're not detaching
if (!opts.detached) {
const logStream = await api.getLogStream();
globalLogger.logInfo('Streaming device logs...');
promises.push(
displayDeviceLogs(logStream, globalLogger, opts.system, opts.services),
);
} else {
promises.push(livepush.init());
if (opts.detached) {
globalLogger.logLivepush(
'Running in detached mode, no service logs will be shown',
);
}
globalLogger.logLivepush('Watching for file changes...');
globalLogger.outputDeferredMessages();
await Promise.all(promises);
} else {
if (opts.detached) {
return;
}
// Print an empty newline to separate the build output
// from the device output
console.log();
// Now all we need to do is stream back the logs
const logStream = await api.getLogStream();
globalLogger.logInfo('Streaming device logs...');
globalLogger.outputDeferredMessages();
await displayDeviceLogs(
logStream,
globalLogger,
opts.system,
opts.services,
);
}
try {
await awaitInterruptibleTask(() => Promise.all(promises));
} finally {
// Stop watching files after log streaming ends (e.g. on SIGINT)
livepush?.close();
await livepush?.cleanup();
}
}
async function streamDeviceLogs(
deviceApi: DeviceAPI,
opts: DeviceDeployOptions,
) {
// Only show logs if we're not detaching
if (opts.detached) {
return;
}
globalLogger.logInfo('Streaming device logs...');
const { connectAndDisplayDeviceLogs } = await import('./logs');
return connectAndDisplayDeviceLogs({
deviceApi,
logger: globalLogger,
system: opts.system || false,
filterServices: opts.services,
maxAttempts: 1001,
});
}
function connectToDocker(host: string, port: number): Docker {
@ -441,7 +449,9 @@ export async function rebuildSingleTask(
);
if (task == null) {
throw new Error(`Could not find build task for service ${serviceName}`);
throw new ExpectedError(
`Could not find build task for service ${serviceName}`,
);
}
await assignDockerBuildOpts(docker, [task], opts);
@ -610,8 +620,6 @@ export function generateTargetState(
}
async function inspectBuildResults(images: LocalImage[]): Promise<void> {
const { exitWithExpectedError } = await import('../../errors');
const failures: LocalPushErrors.BuildFailure[] = [];
_.each(images, (image) => {
@ -624,6 +632,6 @@ async function inspectBuildResults(images: LocalImage[]): Promise<void> {
});
if (failures.length > 0) {
exitWithExpectedError(new LocalPushErrors.BuildError(failures).toString());
throw new LocalPushErrors.BuildError(failures).toString();
}
}

View File

@ -1,12 +1,29 @@
/**
* @license
* Copyright 2018-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 { TypedError } from 'typed-error';
import { ExpectedError } from '../../errors';
export interface BuildFailure {
error: Error;
serviceName: string;
}
export class BuildError extends TypedError {
export class BuildError extends ExpectedError {
private failures: BuildFailure[];
public constructor(failures: BuildFailure[]) {
@ -33,7 +50,7 @@ export class BuildError extends TypedError {
}
}
export class DeviceAPIError extends TypedError {}
export class DeviceAPIError extends ExpectedError {}
export class BadRequestDeviceAPIError extends DeviceAPIError {}
export class ServiceUnavailableAPIError extends DeviceAPIError {}

View File

@ -219,24 +219,17 @@ export class LivepushManager {
this.rebuildsCancelled[serviceName] = false;
}
}
}
// Setup cleanup handlers for the device
// This is necessary because the `exit-hook` module is used by several
// dependencies, and will exit without calling the following handler.
// Once https://github.com/balena-io/balena-cli/issues/867 has been solved,
// we are free to (and definitely should) remove the below line
process.removeAllListeners('SIGINT');
process.on('SIGINT', async () => {
this.logger.logLivepush('Cleaning up device...');
await Promise.all(
_.map(this.containers, (container) => {
container.livepush.cleanupIntermediateContainers();
}),
);
process.exit(0);
});
/** Delete intermediate build containers from the device */
public async cleanup() {
this.logger.logLivepush('Cleaning up device...');
await Promise.all(
_.map(this.containers, (container) =>
container.livepush.cleanupIntermediateContainers(),
),
);
this.logger.logDebug('Cleaning up done.');
}
protected setupFilesystemWatcher(
@ -290,6 +283,17 @@ export class LivepushManager {
return monitor;
}
/** Stop the filesystem watcher, allowing the Node process to exit gracefully */
public close() {
for (const container of Object.values(this.containers)) {
container.monitor.close().catch((err) => {
if (process.env.DEBUG) {
this.logger.logDebug(`chokidar.close() ${err.message}`);
}
});
}
}
public static preprocessDockerfile(content: string): string {
return new Dockerfile(content).generateLiveDockerfile();
}

View File

@ -1,9 +1,33 @@
/**
* @license
* Copyright 2018-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ColorHash = require('color-hash');
import * as _ from 'lodash';
import type { Readable } from 'stream';
import { getChalk } from '../lazy';
import Logger = require('../logger');
import { ExpectedError, SIGINTError } from '../../errors';
class DeviceConnectionLostError extends ExpectedError {
public static defaultMsg = 'Connection to device lost';
constructor(msg?: string) {
super(msg || DeviceConnectionLostError.defaultMsg);
}
}
interface Log {
message: string;
@ -32,23 +56,87 @@ interface BuildLog {
* @param filterService Filter the logs so that only logs
* from a single service will be displayed
*/
export function displayDeviceLogs(
async function displayDeviceLogs(
logs: Readable,
logger: Logger,
system: boolean,
filterServices?: string[],
): Promise<void> {
return new Promise((resolve, reject) => {
logs.on('data', (log) => {
displayLogLine(log, logger, system, filterServices);
const { addSIGINTHandler } = await import('../helpers');
const { parse: ndjsonParse } = await import('ndjson');
let gotSignal = false;
const handleSignal = () => {
gotSignal = true;
logs.emit('close');
};
addSIGINTHandler(handleSignal);
process.once('SIGTERM', handleSignal);
try {
await new Promise((_resolve, reject) => {
const jsonStream = ndjsonParse();
jsonStream.on('data', (log) => {
displayLogObject(log, logger, system, filterServices);
});
jsonStream.on('error', (e) => {
logger.logWarn(`Error parsing NDJSON log chunk: ${e}`);
});
logs.once('error', reject);
logs.once('end', () => {
logger.logWarn(DeviceConnectionLostError.defaultMsg);
if (gotSignal) {
reject(new SIGINTError('Log streaming aborted on SIGINT signal'));
} else {
reject(new DeviceConnectionLostError());
}
});
logs.pipe(jsonStream);
});
} finally {
process.removeListener('SIGINT', handleSignal);
process.removeListener('SIGTERM', handleSignal);
}
}
logs.on('error', reject);
logs.on('end', () => {
logger.logError('Connection to device lost');
resolve();
/**
* Open a TCP connection to the device's supervisor (TCP port 48484) and tail
* (display) device logs. Retry (reconnect) up to maxAttempts times if the
* TCP connection drops. Don't retry on SIGINT (CTRL-C).
* See function `displayDeviceLogs` for parameter documentation.
*/
export async function connectAndDisplayDeviceLogs({
deviceApi,
logger,
system,
filterServices,
maxAttempts = 3,
}: {
deviceApi: import('./api').DeviceAPI;
logger: Logger;
system: boolean;
filterServices?: string[];
maxAttempts?: number;
}) {
async function connectAndDisplay() {
// Open a new connection to the device's supervisor, TCP port 48484
const logStream = await deviceApi.getLogStream();
return displayDeviceLogs(logStream, logger, system, filterServices);
}
const { retry } = await import('../../utils/helpers');
try {
await retry({
func: connectAndDisplay,
maxAttempts,
label: 'Streaming logs',
});
});
} catch (err) {
if (err instanceof DeviceConnectionLostError) {
err.message = `Max retry count (${
maxAttempts - 1
}) exceeded while attempting to reconnect to the device`;
}
throw err;
}
}
export function displayBuildLog(log: BuildLog, logger: Logger): void {
@ -58,21 +146,6 @@ export function displayBuildLog(log: BuildLog, logger: Logger): void {
logger.logBuild(toPrint);
}
// mutates serviceColours
function displayLogLine(
log: string | Buffer,
logger: Logger,
system: boolean,
filterServices?: string[],
): void {
try {
const obj: Log = JSON.parse(log.toString());
displayLogObject(obj, logger, system, filterServices);
} catch (e) {
logger.logDebug(`Dropping device log due to failed parsing: ${e}`);
}
}
export function displayLogObject<T extends Log>(
obj: T,
logger: Logger,

View File

@ -50,7 +50,7 @@ export async function performLocalDeviceSSH(
});
const regex = new RegExp(`(^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
const nameRegex = /\/?([a-zA-Z0-9_]+)_\d+_\d+/;
const nameRegex = /\/?([a-zA-Z0-9_-]+)_\d+_\d+/;
let allContainers: ContainerInfo[];
try {
allContainers = await docker.listContainers();

View File

@ -79,7 +79,7 @@ export const dockerCliFlags: flags.Input<DockerCliFlags> = {
}),
buildArg: flags.string({
description:
'Set a build-time variable (eg. "-B \'ARG=value\'"). Can be specified multiple times.',
'[Deprecated] Set a build-time variable (eg. "-B \'ARG=value\'"). Can be specified multiple times.',
char: 'B',
multiple: true,
}),

View File

@ -17,7 +17,6 @@
import { promises as fs } from 'fs';
import Logger = require('./logger');
import { isV12 } from './version';
const globalLogger = Logger.getLogger();
@ -111,9 +110,7 @@ export async function readFileWithEolConversion(
);
// And summary warning later
globalLogger.deferredLog(
isV12()
? 'Windows-format line endings were detected in some files, but were not converted due to `--noconvert-eol` option.'
: 'Windows-format line endings were detected in some files. Consider using the `--convert-eol` option.',
'Windows-format line endings were detected in some files, but were not converted due to `--noconvert-eol` option.',
Logger.Level.WARN,
);

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2020 Balena
Copyright 2016-2021 Balena Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,16 +16,11 @@ limitations under the License.
import type { InitializeEmitter, OperationState } from 'balena-device-init';
import type * as BalenaSdk from 'balena-sdk';
import { spawn, SpawnOptions } from 'child_process';
import * as _ from 'lodash';
import * as os from 'os';
import type * as ShellEscape from 'shell-escape';
import type { Device, PineOptions } from 'balena-sdk';
import { ExpectedError } from '../errors';
import { getBalenaSdk, getChalk, getVisuals } from './lazy';
import * as _ from 'lodash';
import { promisify } from 'util';
import { isSubcommand } from '../preparser';
import { getBalenaSdk, getChalk, getVisuals } from './lazy';
export function getGroupDefaults(group: {
options: Array<{ name: string; default: string | number }>;
@ -84,7 +79,7 @@ export async function sudo(
) {
const { executeWithPrivileges } = await import('./sudo');
if (os.platform() !== 'win32') {
if (process.platform !== 'win32') {
console.log(
msg ||
'Admin privileges required: you may be asked for your computer password to continue.',
@ -95,6 +90,9 @@ export async function sudo(
}
export function runCommand<T>(commandArgs: string[]): Promise<T> {
const {
isSubcommand,
} = require('../preparser') as typeof import('../preparser');
if (isSubcommand(commandArgs)) {
commandArgs = [
commandArgs[0] + ':' + commandArgs[1],
@ -204,39 +202,67 @@ function getApplication(
) as Promise<ApplicationWithDeviceType>;
}
const second = 1000; // 1000 milliseconds
const minute = 60 * second;
export const delay = promisify(setTimeout);
/**
* Call `func`, and if func() throws an error or returns a promise that
* eventually rejects, retry it `times` many times, each time printing a
* log message including the given `label` and the error that led to
* retrying. Wait delayMs before the first retry, multiplying the wait
* by backoffScaler for each further attempt.
* eventually rejects, retry it `times` many times, each time printing a log
* message including the given `label` and the error that led to retrying.
* Wait initialDelayMs before the first retry. Before each further retry,
* the delay is reduced by the time elapsed since the last retry, and
* increased by multiplying the result by backoffScaler.
* @param func: The function to call and, if needed, retry calling
* @param times: How many times to retry calling func()
* @param maxAttempts: How many times (max) to try calling func().
* func() will always be called at least once.
* @param label: Label to include in the retry log message
* @param startingDelayMs: How long to wait before the first retry
* @param initialDelayMs: How long to wait before the first retry
* @param backoffScaler: Multiplier to previous wait time
* @param count: Used "internally" for the recursive calls
* @param maxSingleDelayMs: Maximum interval between retries
*/
export async function retry<T>(
func: () => T,
times: number,
label: string,
startingDelayMs = 1000,
export async function retry<T>({
func,
maxAttempts,
label,
initialDelayMs = 1000,
backoffScaler = 2,
): Promise<T> {
for (let count = 0; count < times - 1; count++) {
maxSingleDelayMs = 1 * minute,
}: {
func: () => T;
maxAttempts: number;
label: string;
initialDelayMs?: number;
backoffScaler?: number;
maxSingleDelayMs?: number;
}): Promise<T> {
const { SIGINTError } = await import('../errors');
let delayMs = initialDelayMs;
for (let count = 0; count < maxAttempts - 1; count++) {
const lastAttemptMs = Date.now();
try {
return await func();
} catch (err) {
const delayMS = backoffScaler ** count * startingDelayMs;
// Don't retry on SIGINT (CTRL-C)
if (err instanceof SIGINTError) {
throw err;
}
if (count) {
// use Math.max to work around system time changes, e.g. DST
const elapsedMs = Math.max(0, Date.now() - lastAttemptMs);
// reduce delayMs by the time elapsed since the last attempt
delayMs = Math.max(initialDelayMs, delayMs - elapsedMs);
// increase delayMs by the backoffScaler factor
delayMs = Math.min(maxSingleDelayMs, delayMs * backoffScaler);
}
const sec = delayMs / 1000;
const secStr = sec < 10 ? sec.toFixed(1) : Math.round(sec).toString();
console.log(
`Retrying "${label}" after ${(delayMS / 1000).toFixed(2)}s (${
count + 1
} of ${times}) due to: ${err}`,
`Retrying "${label}" after ${secStr}s (${count + 1} of ${
maxAttempts - 1
}) due to: ${err}`,
);
await delay(delayMS);
await delay(delayMs);
}
}
return await func();
@ -321,7 +347,7 @@ export function shellEscape(args: string[], detectShell = false): string[] {
if (isCmdExe) {
return args.map((v) => windowsCmdExeEscapeArg(v));
} else {
const shellEscapeFunc: typeof ShellEscape = require('shell-escape');
const shellEscapeFunc: typeof import('shell-escape') = require('shell-escape');
return args.map((v) => shellEscapeFunc([v]));
}
}
@ -365,6 +391,7 @@ export async function which(
} catch (err) {
if (err.code === 'ENOENT') {
if (rejectOnMissing) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
`'${program}' program not found. Is it installed?`,
);
@ -395,9 +422,10 @@ export async function which(
export async function whichSpawn(
programName: string,
args: string[],
options: SpawnOptions = { stdio: 'inherit' },
options: import('child_process').SpawnOptions = { stdio: 'inherit' },
returnExitCodeOrSignal = false,
): Promise<[number | undefined, string | undefined]> {
const { spawn } = await import('child_process');
const program = await which(programName);
if (process.env.DEBUG) {
console.error(`[debug] [${program}, ${args.join(', ')}]`);
@ -483,10 +511,84 @@ export function getProxyConfig(): ProxyConfig | undefined {
}
}
export const expandForAppName: PineOptions<Device> = {
export const expandForAppName: BalenaSdk.PineOptions<BalenaSdk.Device> = {
$expand: {
belongs_to__application: { $select: 'app_name' },
is_of__device_type: { $select: 'slug' },
is_running__release: { $select: 'commit' },
},
};
/**
* Use the `readline` library on Windows to install SIGINT handlers.
* This appears to be necessary on MSYS / Git for Windows, and also useful
* with PowerShell to avoid the built-in "Terminate batch job? (Y/N)" prompt
* that appears to result in ungraceful / abrupt process termination.
*/
const installReadlineSigintEmitter = _.once(function emitSigint() {
if (process.platform === 'win32') {
const readline = require('readline') as typeof import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on('SIGINT', () => process.emit('SIGINT' as any));
}
});
/**
* Centralized cross-platform logic to install a SIGINT handler
* @param sigintHandler The handler function
* @param once Whether the handler should be called no more than once
*/
export function addSIGINTHandler(sigintHandler: () => void, once = true) {
installReadlineSigintEmitter();
if (once) {
process.once('SIGINT', sigintHandler);
} else {
process.on('SIGINT', sigintHandler);
}
}
/**
* Call the given task function (which returns a promise) with the given
* arguments, await the returned promise and resolve to the same result.
* While awaiting for that promise, also await for a SIGINT signal (if any),
* with a new SIGINT handler that is automatically removed on return.
* If a SIGINT signal is received while awaiting for the task function,
* immediately return a promise that rejects with SIGINTError.
* @param task An async function to be executed and awaited
* @param theArgs Arguments to be passed to the task function
*/
export async function awaitInterruptibleTask<
T extends (...args: any[]) => Promise<any>
>(task: T, ...theArgs: Parameters<T>): Promise<ReturnType<T>> {
let sigintHandler: () => void = () => undefined;
const sigintPromise = new Promise<T>((_resolve, reject) => {
sigintHandler = () => {
const {
SIGINTError,
} = require('../errors') as typeof import('../errors');
reject(new SIGINTError('Task aborted on SIGINT signal'));
};
addSIGINTHandler(sigintHandler);
});
try {
return await Promise.race([sigintPromise, task(...theArgs)]);
} finally {
process.removeListener('SIGINT', sigintHandler);
}
}
/** Check if `drive` is mounted, and if so umount it. No-op on Windows. */
export async function safeUmount(drive: string) {
if (!drive) {
return;
}
const { isMounted, umount } = await import('umount');
const isMountedAsync = promisify(isMounted);
if (await isMountedAsync(drive)) {
const umountAsync = promisify(umount);
await umountAsync(drive);
}
}

View File

@ -38,9 +38,9 @@ export const balenaAsciiArt = `\
|_.__/ \\__,_||_| \\____/|_| |_| \\__,_|
`;
export const registrySecretsHelp = `\
REGISTRY SECRETS
The --registry-secrets option specifies a JSON or YAML file containing private
export const registrySecretsHelp =
'REGISTRY SECRETS \n' +
`The --registry-secrets option specifies a JSON or YAML file containing private
Docker registry usernames and passwords to be used when pulling base images.
Sample registry-secrets YAML file:
\`\`\`
@ -61,9 +61,9 @@ If the --registry-secrets option is not specified, and a secrets.yml or
secrets.json file exists in the balena directory (usually $HOME/.balena),
this file will be used instead.`;
export const dockerignoreHelp = `\
DOCKERIGNORE AND GITIGNORE FILES
By default, the balena CLI will use a single ".dockerignore" file (if any) at
export const dockerignoreHelp =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
the project root (--source directory) in order to decide which source files to
exclude from the "build context" (tar stream) sent to balenaCloud, Docker
daemon or balenaEngine. In a microservices (multicontainer) application, the
@ -83,7 +83,7 @@ any) defined at the overall project root will be used to filter files and
subdirectories other than service subdirectories. It will not have any effect
on service subdirectories, whether or not a service subdirectory defines its
own .dockerignore file. Multiple .dockerignore files are not merged or added
together, and cannot override or extend other files. This behavior maximises
together, and cannot override or extend other files. This behavior maximizes
compatibility with the standard docker-compose tool, while still allowing a
root .dockerignore file (at the overall project root) to filter files and
folders that are outside service subdirectories.
@ -94,8 +94,8 @@ option if compatibility is required. This option is mutually exclusive with
--multi-dockerignore (-m) and will be removed in the CLI's next major version
release (v13).
Default .dockerignore patterns
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
Default .dockerignore patterns \n` +
`When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
patterns found in the applicable .dockerignore files, in the following order:
\`\`\`
@ -113,3 +113,33 @@ adding counter patterns to the applicable .dockerignore file(s), for example
\`!mysubmodule/.git\`. For documentation on pattern format, see:
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore`;
export const applicationIdInfo = `\
Applications may be specified by app name, slug, or numeric ID. App slugs
are the recommended option, as they are unique and unambiguous. Slugs
can be listed with the \`balena apps\` command. Note that slugs may change
if the application is renamed.
App names are not unique and may result in "Application is ambiguous" errors
at any time (even if it "used to work in the past"), for example if the name
clashes with a newly created public application, or with apps from other balena
accounts that you may have been invited to as a member. For this reason, app
names are especially discouraged in scripts (e.g. CI environments).
Numeric app IDs are deprecated because they consist of an implementation detail
of the balena backend. We intend to remove support for numeric IDs at some point
in the future.`;
export const jsonInfo = `\
The --json option is recommended when scripting the output of this command,
because field names are less likely to change in JSON format and because it
better represents data types like arrays, empty strings and null values.
The 'jq' utility may be helpful for querying JSON fields in shell scripts
(https://stedolan.github.io/jq/manual/).`;
export const buildArgDeprecation = `\
WARNING: You have specified a '--buildArg' option, which is now deprecated, and
may be removed in the future. The recommended alternative is build-time secrets:
https://www.balena.io/docs/learn/deploy/deployment/#build-time-secrets-and-variables
If you have a particular use for buildArg, which is not satisfied by build-time
secrets, please contact us via support or the forums: https://forums.balena.io/
\n`;

View File

@ -16,19 +16,8 @@
*/
import type { BalenaSDK } from 'balena-sdk';
import _ = require('lodash');
import { ExpectedError } from '../errors';
export function normalizeUuidProp(
params: { [key: string]: any },
propName = 'uuid',
) {
if (typeof params[propName] === 'number') {
params[propName] =
params[propName + '_raw'] || _.toString(params[propName]);
}
}
/**
* Takes a string which may represent one of:
* - Integer release id
@ -85,3 +74,10 @@ export async function disambiguateReleaseParam(
// Must be a number only uuid/hash (or nonexistent release)
return (await balena.models.release.get(release, { $select: 'id' })).id;
}
/**
* Convert to lowercase if looks like slug
*/
export function lowercaseIfSlug(s: string) {
return s.includes('/') ? s.toLowerCase() : s;
}

View File

@ -25,6 +25,9 @@ import {
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
import validation = require('./validation');
import { delay } from './helpers';
import { isV13 } from './version';
import type { Application, Device, Organization } from 'balena-sdk';
import { getApplication } from './sdk';
export function authenticate(options: {}): Promise<void> {
const balena = getBalenaSdk();
@ -147,7 +150,11 @@ export async function confirm(
) {
if (yesOption) {
if (yesMessage) {
console.log(yesMessage);
if (isV13()) {
console.error(yesMessage);
} else {
console.log(yesMessage);
}
}
return;
}
@ -159,7 +166,8 @@ export async function confirm(
});
if (!confirmed) {
const err = new Error('Aborted');
const err = new ExpectedError('Aborted');
// TODO remove this deprecated function (exitWithExpectedError)
if (exitIfDeclined) {
exitWithExpectedError(err);
}
@ -180,7 +188,6 @@ export function selectApplication(
}
const apps = (await balena.models.application.getAll({
$select: 'app_name',
$expand: {
is_for__device_type: {
$select: 'slug',
@ -197,61 +204,25 @@ export function selectApplication(
message: 'Select an application',
type: 'list',
choices: _.map(applications, (application) => ({
name: `${application.app_name} (${application.is_for__device_type[0].slug})`,
value: application.app_name,
name: `${application.app_name} (${application.slug}) [${application.is_for__device_type[0].slug}]`,
value: application,
})),
});
});
}
export function selectOrCreateApplication() {
const balena = getBalenaSdk();
return balena.models.application
.hasAny()
.then((hasAnyApplications) => {
if (!hasAnyApplications) {
// Just to make TS happy
return Promise.resolve(undefined);
}
return (balena.models.application.getAll({
$expand: {
is_for__device_type: {
$select: 'slug',
},
},
}) as Promise<ApplicationWithDeviceType[]>).then((applications) => {
const appOptions: Array<{ name: string; value: string | null }> = _.map(
applications,
(application) => ({
name: `${application.app_name} (${application.is_for__device_type[0].slug})`,
value: application.app_name,
}),
);
appOptions.unshift({
name: 'Create a new application',
value: null,
});
return getCliForm().ask({
message: 'Select an application',
type: 'list',
choices: appOptions,
});
});
})
.then((application) => {
if (application) {
return application;
}
return getCliForm().ask({
message: 'Choose a Name for your new application',
type: 'input',
validate: validation.validateApplicationName,
});
});
export async function selectOrganization(organizations?: Organization[]) {
// Use either provided orgs (if e.g. already loaded) or load from cloud
organizations =
organizations || (await getBalenaSdk().models.organization.getAll());
return getCliForm().ask({
message: 'Select an organization',
type: 'list',
choices: organizations.map((org) => ({
name: `${org.name} (${org.handle})`,
value: org.handle,
})),
});
}
export async function awaitDevice(uuid: string) {
@ -359,99 +330,94 @@ export function inferOrSelectDevice(preferredUuid: string) {
});
}
async function getApplicationByIdOrName(
sdk: BalenaSdk.BalenaSDK,
idOrName: string,
) {
if (validation.looksLikeInteger(idOrName)) {
try {
return await sdk.models.application.get(Number(idOrName));
} catch (error) {
const { BalenaApplicationNotFound } = await import('balena-errors');
if (!instanceOf(error, BalenaApplicationNotFound)) {
throw error;
}
}
}
return await sdk.models.application.get(idOrName);
}
export async function getOnlineTargetUuid(
/*
* Given applicationOrDevice, which may be
* - an application name
* - an application slug
* - an application id (integer)
* - a device uuid
* Either:
* - in case of device uuid, return uuid of device after verifying that it exists and is online.
* - in case of application, return uuid of device user selects from list of online devices.
*
* TODO: Modify this when app IDs dropped.
*/
export async function getOnlineTargetDeviceUuid(
sdk: BalenaSdk.BalenaSDK,
applicationOrDevice: string,
) {
// applicationOrDevice can be:
// * an application name
// * an application ID (integer)
// * a device uuid
const Logger = await import('../utils/logger');
const logger = Logger.getLogger();
const appTest = validation.validateApplicationName(applicationOrDevice);
const uuidTest = validation.validateUuid(applicationOrDevice);
const logger = (await import('../utils/logger')).getLogger();
if (!appTest && !uuidTest) {
throw new ExpectedError(
`Device or application not found: ${applicationOrDevice}`,
);
// If looks like UUID, probably device
if (validation.validateUuid(applicationOrDevice)) {
let device: Device;
try {
logger.logDebug(
`Trying to fetch device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
);
device = await sdk.models.device.get(applicationOrDevice, {
$select: ['uuid', 'is_online'],
});
if (!device.is_online) {
throw new ExpectedError(
`Device with UUID ${applicationOrDevice} is offline`,
);
}
return device.uuid;
} catch (err) {
const { BalenaDeviceNotFound } = await import('balena-errors');
if (instanceOf(err, BalenaDeviceNotFound)) {
logger.logDebug(`Device with UUID ${applicationOrDevice} not found`);
// Now try app
} else {
throw err;
}
}
}
// if we have a definite device UUID...
if (uuidTest && !appTest) {
logger.logDebug(
`Fetching device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
);
return (
await sdk.models.device.get(applicationOrDevice, {
$select: ['uuid'],
$filter: { is_online: true },
})
).uuid;
}
// otherwise, it may be a device OR an application...
// Not a device UUID, try app
let app: Application;
try {
logger.logDebug(
`Fetching application by ID or name ${applicationOrDevice} (${typeof applicationOrDevice})`,
`Trying to fetch application by name/slug/ID: ${applicationOrDevice}`,
);
const app = await getApplicationByIdOrName(sdk, applicationOrDevice);
const devices = await sdk.models.device.getAllByApplication(app.id, {
$filter: { is_online: true },
});
if (_.isEmpty(devices)) {
throw new ExpectedError('No accessible devices are online');
}
return await getCliForm().ask({
message: 'Select a device',
type: 'list',
default: devices[0].uuid,
choices: _.map(devices, (device) => ({
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(
0,
7,
)})`,
value: device.uuid,
})),
});
app = await getApplication(sdk, applicationOrDevice);
} catch (err) {
const { BalenaApplicationNotFound } = await import('balena-errors');
if (!instanceOf(err, BalenaApplicationNotFound)) {
if (instanceOf(err, BalenaApplicationNotFound)) {
throw new ExpectedError(
`Application or Device not found: ${applicationOrDevice}`,
);
} else {
throw err;
}
logger.logDebug(`Application not found`);
}
// it wasn't an application, maybe it's a device...
logger.logDebug(
`Fetching device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
);
return (
await sdk.models.device.get(applicationOrDevice, {
$select: ['uuid'],
$filter: { is_online: true },
})
).uuid;
// App found, load its devices
const devices = await sdk.models.device.getAllByApplication(app.id, {
$select: ['device_name', 'uuid'],
$filter: { is_online: true },
});
// Throw if no devices online
if (_.isEmpty(devices)) {
throw new ExpectedError(
`Application ${app.slug} found, but has no devices online.`,
);
}
// Ask user to select from online devices for application
return getCliForm().ask({
message: `Select a device on application ${app.slug}`,
type: 'list',
default: devices[0].uuid,
choices: _.map(devices, (device) => ({
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(0, 7)})`,
value: device.uuid,
})),
});
}
export function selectFromList<T>(

View File

@ -31,12 +31,12 @@ export async function join(
appUpdatePollInterval?: number,
): Promise<void> {
logger.logDebug('Determining device...');
const deviceIp = await getOrSelectLocalDevice(deviceHostnameOrIp);
await assertDeviceIsCompatible(deviceIp);
logger.logDebug(`Using device: ${deviceIp}`);
deviceHostnameOrIp = deviceHostnameOrIp || (await selectLocalDevice());
await assertDeviceIsCompatible(deviceHostnameOrIp);
logger.logDebug(`Using device: ${deviceHostnameOrIp}`);
logger.logDebug('Determining device type...');
const deviceType = await getDeviceType(deviceIp);
const deviceType = await getDeviceType(deviceHostnameOrIp);
logger.logDebug(`Device type: ${deviceType}`);
logger.logDebug('Determining application...');
@ -50,7 +50,7 @@ export async function join(
}
logger.logDebug('Determining device OS version...');
const deviceOsVersion = await getOsVersion(deviceIp);
const deviceOsVersion = await getOsVersion(deviceHostnameOrIp);
logger.logDebug(`Device OS version: ${deviceOsVersion}`);
logger.logDebug('Generating application config...');
@ -61,8 +61,7 @@ export async function join(
logger.logDebug(`Using config: ${JSON.stringify(config, null, 2)}`);
logger.logDebug('Configuring...');
await configure(deviceIp, config);
logger.logDebug('All done.');
await configure(deviceHostnameOrIp, config);
const platformUrl = await sdk.settings.get('balenaUrl');
logger.logSuccess(`Device successfully joined ${platformUrl}!`);
@ -70,17 +69,15 @@ export async function join(
export async function leave(
logger: Logger,
_sdk: BalenaSdk.BalenaSDK,
deviceHostnameOrIp?: string,
): Promise<void> {
logger.logDebug('Determining device...');
const deviceIp = await getOrSelectLocalDevice(deviceHostnameOrIp);
await assertDeviceIsCompatible(deviceIp);
logger.logDebug(`Using device: ${deviceIp}`);
deviceHostnameOrIp = deviceHostnameOrIp || (await selectLocalDevice());
await assertDeviceIsCompatible(deviceHostnameOrIp);
logger.logDebug(`Using device: ${deviceHostnameOrIp}`);
logger.logDebug('Deconfiguring...');
await deconfigure(deviceIp);
logger.logDebug('All done.');
await deconfigure(deviceHostnameOrIp);
logger.logSuccess('Device successfully left the platform.');
}
@ -155,39 +152,21 @@ async function getOsVersion(deviceIp: string): Promise<string> {
return match[1];
}
async function getOrSelectLocalDevice(deviceIp?: string): Promise<string> {
if (deviceIp) {
return deviceIp;
}
const through = await import('through2');
let ip: string | null = null;
const stream = through(function (data, _enc, cb) {
const match = /^==> Selected device: (.*)$/m.exec(data.toString());
if (match) {
ip = match[1];
cb();
async function selectLocalDevice(): Promise<string> {
const { forms } = await import('balena-sync');
let hostnameOrIp;
try {
hostnameOrIp = await forms.selectLocalBalenaOsDevice();
console.error(`==> Selected device: ${hostnameOrIp}`);
} catch (e) {
if (e.message.toLowerCase().includes('could not find any')) {
throw new ExpectedError(e);
} else {
cb(null, data);
throw e;
}
});
stream.pipe(process.stderr);
const { sudo } = await import('../utils/helpers');
const command = ['internal', 'scandevices'];
await sudo(command, {
stderr: stream,
msg:
'Scanning for local devices. If asked, please type your computer password.',
});
if (!ip) {
throw new ExpectedError('No device selected');
}
return ip;
return hostnameOrIp;
}
async function selectAppFromList(

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2017-2020 Balena Ltd.
* Copyright 2017-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,10 +16,12 @@
*/
import type * as Dockerode from 'dockerode';
import { ExpectedError } from '../errors';
import { getBalenaSdk, stripIndent } from './lazy';
import Logger = require('./logger');
export const QEMU_VERSION = 'v4.0.0+balena2';
export const QEMU_VERSION = 'v5.2.0+balena4';
export const QEMU_BIN_NAME = 'qemu-execve';
export function qemuPathInContext(context: string) {
@ -63,7 +65,8 @@ export function copyQemu(context: string, arch: string) {
.then(() => path.relative(context, binPath));
}
export const getQemuPath = function (arch: string) {
export const getQemuPath = function (balenaArch: string) {
const qemuArch = balenaArchToQemuArch(balenaArch);
const balena = getBalenaSdk();
const path = require('path') as typeof import('path');
const { promises: fs } = require('fs') as typeof import('fs');
@ -79,64 +82,76 @@ export const getQemuPath = function (arch: string) {
throw err;
})
.then(() =>
path.join(binDir, `${QEMU_BIN_NAME}-${arch}-${QEMU_VERSION}`),
path.join(binDir, `${QEMU_BIN_NAME}-${qemuArch}-${QEMU_VERSION}`),
),
);
};
export function installQemu(arch: string) {
const request = require('request') as typeof import('request');
const fs = require('fs') as typeof import('fs');
const zlib = require('zlib') as typeof import('zlib');
const tar = require('tar-stream') as typeof import('tar-stream');
async function installQemu(arch: string, qemuPath: string) {
const qemuArch = balenaArchToQemuArch(arch);
const fileVersion = QEMU_VERSION.replace('v', '').replace('+', '.');
const urlFile = encodeURIComponent(`qemu-${fileVersion}-${qemuArch}.tar.gz`);
const urlVersion = encodeURIComponent(QEMU_VERSION);
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`;
return getQemuPath(arch).then(
(qemuPath) =>
new Promise(function (resolve, reject) {
const installStream = fs.createWriteStream(qemuPath);
const request = await import('request');
const fs = await import('fs');
const zlib = await import('zlib');
const tar = await import('tar-stream');
const qemuArch = balenaArchToQemuArch(arch);
const fileVersion = QEMU_VERSION.replace(/^v/, '').replace('+', '.');
const urlFile = encodeURIComponent(
`qemu-${fileVersion}-${qemuArch}.tar.gz`,
);
const urlVersion = encodeURIComponent(QEMU_VERSION);
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`;
const extract = tar.extract();
extract.on('entry', function (header, stream, next) {
// createWriteStream creates a zero-length file on disk that
// needs to be deleted if the download fails
const installStream = fs.createWriteStream(qemuPath);
try {
await new Promise<void>((resolve, reject) => {
const extract = tar.extract();
extract.on('entry', function (header, stream, next) {
try {
stream.on('end', next);
if (header.name.includes(`qemu-${qemuArch}-static`)) {
stream.pipe(installStream);
} else {
stream.resume();
}
} catch (err) {
reject(err);
}
});
request(qemuUrl)
.on('error', reject)
.pipe(zlib.createGunzip())
.on('error', reject)
.pipe(extract)
.on('error', reject)
.on('finish', function () {
fs.chmodSync(qemuPath, '755');
resolve();
});
return request(qemuUrl)
.on('error', reject)
.pipe(zlib.createGunzip())
.on('error', reject)
.pipe(extract)
.on('error', reject)
.on('finish', function () {
fs.chmodSync(qemuPath, '755');
resolve();
});
}),
);
});
} catch (err) {
try {
await fs.promises.unlink(qemuPath);
} catch {
// ignore
}
throw err;
}
}
const balenaArchToQemuArch = function (arch: string) {
switch (arch) {
case 'armv7hf':
case 'rpi':
case 'arm':
case 'armhf':
case 'armv7hf':
return 'arm';
case 'arm64':
case 'aarch64':
return 'aarch64';
default:
throw new Error(`Cannot install emulator for architecture ${arch}`);
throw new ExpectedError(stripIndent`
Unknown ARM architecture identifier "${arch}".
Known ARM identifiers: rpi arm armhf armv7hf arm64 aarch64`);
}
};
@ -155,11 +170,17 @@ export async function installQemuIfNeeded(
const { promises: fs } = await import('fs');
const qemuPath = await getQemuPath(arch);
try {
const stats = await fs.stat(qemuPath);
// Earlier versions of the CLI with broken error handling would leave
// behind files with size 0. If such a file is found, delete it.
if (stats.size === 0) {
await fs.unlink(qemuPath);
}
await fs.access(qemuPath);
} catch {
// Qemu doesn't exist so install it
// QEMU not found in cache folder (~/.balena/bin/), so install it
logger.logInfo(`Installing qemu for ${arch} emulation...`);
await installQemu(arch);
await installQemu(arch, qemuPath);
}
return true;
}

View File

@ -22,8 +22,7 @@ import type * as Stream from 'stream';
import streamToPromise = require('stream-to-promise');
import type { Pack } from 'tar-stream';
import { ExpectedError } from '../errors';
import { exitWithExpectedError } from '../errors';
import { ExpectedError, SIGINTError } from '../errors';
import { tarDirectory } from './compose_ts';
import { getVisuals, stripIndent } from './lazy';
import Logger = require('./logger');
@ -46,16 +45,13 @@ export interface BuildOpts {
}
export interface RemoteBuild {
app: string;
owner: string;
appSlug: string;
source: string;
auth: string;
baseUrl: string;
nogitignore: boolean;
opts: BuildOpts;
sdk: BalenaSDK;
// For internal use
releaseId?: number;
hadError?: boolean;
@ -86,14 +82,12 @@ export class RemoteBuildFailedError extends ExpectedError {
async function getBuilderEndpoint(
baseUrl: string,
owner: string,
app: string,
appSlug: string,
opts: BuildOpts,
): Promise<string> {
const querystring = await import('querystring');
const args = querystring.stringify({
owner,
app,
slug: appSlug,
dockerfilePath: opts.dockerfilePath,
emulated: opts.emulated,
nocache: opts.nocache,
@ -109,72 +103,87 @@ async function getBuilderEndpoint(
return `${builderUrl}/v3/build?${args}`;
}
export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
const stream = await getRemoteBuildStream(build);
export async function startRemoteBuild(
build: RemoteBuild,
): Promise<number | undefined> {
const [buildRequest, stream] = await getRemoteBuildStream(build);
// Special windows handling (win64 also reports win32)
if (process.platform === 'win32') {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Setup CTRL-C handler so the user can interrupt the build
let cancellationPromise = Promise.resolve();
const sigintHandler = () => {
process.exitCode = 130;
console.error('\nReceived SIGINT, cleaning up. Please wait.');
try {
cancellationPromise = cancelBuildIfNecessary(build);
} catch (err) {
console.error(err.message);
} finally {
buildRequest.abort();
const sigintErr = new SIGINTError('Build aborted on SIGINT signal');
sigintErr.code = 'SIGINT';
stream.emit('error', sigintErr);
}
};
rl.on('SIGINT', () => process.emit('SIGINT' as any));
const { addSIGINTHandler } = await import('./helpers');
addSIGINTHandler(sigintHandler);
try {
if (build.opts.headless) {
await handleHeadlessBuildStream(build, stream);
} else {
await handleRemoteBuildStream(build, stream);
}
} finally {
process.removeListener('SIGINT', sigintHandler);
globalLogger.outputDeferredMessages();
await cancellationPromise;
}
return build.releaseId;
}
if (!build.opts.headless) {
return new Promise((resolve, reject) => {
// Setup interrupt handlers so we can cancel the build if the user presses
// ctrl+c
// This is necessary because the `exit-hook` module is used by several
// dependencies, and will exit without calling the following handler.
// Once https://github.com/balena-io/balena-cli/issues/867 has been solved,
// we are free to (and definitely should) remove the below line
process.removeAllListeners('SIGINT');
process.on('SIGINT', () => {
process.stderr.write('Received SIGINT, cleaning up. Please wait.\n');
cancelBuildIfNecessary(build).then(() => {
stream.end();
process.exit(130);
});
});
stream.on('data', getBuilderMessageHandler(build));
stream.on('end', resolve);
stream.on('error', reject);
}).then(() => {
globalLogger.outputDeferredMessages();
if (build.hadError) {
throw new RemoteBuildFailedError();
}
});
async function handleRemoteBuildStream(
build: RemoteBuild,
stream: Stream.Stream,
) {
await new Promise((resolve, reject) => {
const msgHandler = getBuilderMessageHandler(build);
stream.on('data', msgHandler);
stream.once('end', resolve);
stream.once('error', reject);
});
if (build.hadError) {
throw new RemoteBuildFailedError();
}
}
async function handleHeadlessBuildStream(
build: RemoteBuild,
stream: Stream.Stream,
) {
// We're running a headless build, which means we'll
// get a single object back, detailing if the build has
// been started
let result: HeadlessBuilderMessage;
let message: HeadlessBuilderMessage;
try {
const response = await streamToPromise(stream);
result = JSON.parse(response.toString());
const response = await streamToPromise(stream as NodeJS.ReadWriteStream);
message = JSON.parse(response.toString());
} catch (e) {
if (e.code === 'SIGINT') {
throw e;
}
throw new Error(
`There was an error reading the response from the remote builder: ${e}`,
);
}
handleHeadlessBuildMessage(result);
}
function handleHeadlessBuildMessage(message: HeadlessBuilderMessage) {
if (!process.stdout.isTTY) {
process.stdout.write(JSON.stringify(message));
return;
}
if (message.started) {
console.log('Build successfully started');
console.log(` Release ID: ${message.releaseId!}`);
build.releaseId = message.releaseId;
} else {
console.log('Failed to start remote build');
console.log(` Error: ${message.error!}`);
@ -255,6 +264,9 @@ function getBuilderMessageHandler(
async function cancelBuildIfNecessary(build: RemoteBuild): Promise<void> {
if (build.releaseId != null) {
console.error(
`Setting 'cancelled' release status for release ID ${build.releaseId} ...`,
);
await build.sdk.pine.patch({
resource: 'release',
id: build.releaseId,
@ -341,7 +353,7 @@ function createRemoteBuildRequest(
headers: { 'Content-Encoding': 'gzip' },
body: tarStream.pipe(zlib.createGzip({ level: 6 })),
})
.on('error', onError)
.once('error', onError) // `.once` because the handler re-emits
.once('response', (response: request.RequestResponse) => {
if (response.statusCode >= 100 && response.statusCode < 400) {
if (DEBUG_MODE) {
@ -357,28 +369,30 @@ function createRemoteBuildRequest(
if (response.body) {
msgArr.push(response.body);
}
onError(new Error(msgArr.join('\n')));
onError(new ExpectedError(msgArr.join('\n')));
}
});
}
async function getRemoteBuildStream(
build: RemoteBuild,
): Promise<NodeJS.ReadWriteStream> {
): Promise<[request.Request, Stream.Stream]> {
const builderUrl = await getBuilderEndpoint(
build.baseUrl,
build.owner,
build.app,
build.appSlug,
build.opts,
);
let stream: Stream.Stream;
let uploadSpinner = {
stop: () => {
/* noop */
},
};
let exitOnError = (error: Error) => {
return exitWithExpectedError(error);
const onError = (error: Error) => {
uploadSpinner.stop();
if (stream) {
stream.emit('error', error);
}
};
// We only show the spinner when outputting to a tty
if (process.stdout.isTTY) {
@ -386,36 +400,26 @@ async function getRemoteBuildStream(
uploadSpinner = new visuals.Spinner(
'Uploading source package to balenaCloud',
);
exitOnError = (error: Error): never => {
uploadSpinner.stop();
return exitWithExpectedError(error);
};
// This is not strongly typed to start with, so we cast
// to any to allow the method call
(uploadSpinner as any).start();
}
try {
const tarStream = await getTarStream(build);
const buildRequest = createRemoteBuildRequest(
build,
tarStream,
builderUrl,
exitOnError,
);
let stream: NodeJS.ReadWriteStream;
if (build.opts.headless) {
stream = (buildRequest as unknown) as NodeJS.ReadWriteStream;
} else {
stream = buildRequest.pipe(JSONStream.parse('*'));
}
return stream
.once('close', () => uploadSpinner.stop())
.once('data', () => uploadSpinner.stop())
.once('end', () => uploadSpinner.stop())
.once('error', () => uploadSpinner.stop())
.once('finish', () => uploadSpinner.stop());
} catch (error) {
return exitOnError(error);
const tarStream = await getTarStream(build);
const buildRequest = createRemoteBuildRequest(
build,
tarStream,
builderUrl,
onError,
);
if (build.opts.headless) {
stream = buildRequest;
} else {
stream = buildRequest.pipe(JSONStream.parse('*'));
}
stream = stream
.once('error', () => uploadSpinner.stop())
.once('close', () => uploadSpinner.stop())
.once('data', () => uploadSpinner.stop())
.once('end', () => uploadSpinner.stop())
.once('finish', () => uploadSpinner.stop());
return [buildRequest, stream];
}

View File

@ -15,7 +15,12 @@
* limitations under the License.
*/
import type { Application, BalenaSDK, PineOptions } from 'balena-sdk';
import type {
Application,
BalenaSDK,
Organization,
PineOptions,
} from 'balena-sdk';
/**
* Wraps the sdk application.get method,
@ -27,10 +32,6 @@ export async function getApplication(
nameOrSlugOrId: string | number,
options?: PineOptions<Application>,
): Promise<Application> {
// TODO: Consider whether it would be useful to generally include interactive selection of application here,
// when nameOrSlugOrId not provided.
// e.g. nameOrSlugOrId || (await (await import('../../utils/patterns')).selectApplication()),
// See commands/device/init.ts ~ln100 for example
const { looksLikeInteger } = await import('./validation');
if (looksLikeInteger(nameOrSlugOrId as string)) {
try {
@ -47,3 +48,67 @@ export async function getApplication(
}
return sdk.models.application.get(nameOrSlugOrId, options);
}
/**
* Given an string representation of an application identifier,
* which could be one of:
* - name (including numeric names)
* - slug
* - numerical id
* disambiguate and return a properly typed identifier.
*
* Attempts to minimise the number of API calls required.
* TODO: Remove this once support for numeric App IDs is removed.
*/
export async function getTypedApplicationIdentifier(
sdk: BalenaSDK,
nameOrSlugOrId: string,
) {
const { looksLikeInteger } = await import('./validation');
// If there's no possible ambiguity,
// return the passed identifier unchanged
if (!looksLikeInteger(nameOrSlugOrId)) {
return nameOrSlugOrId;
}
// Resolve ambiguity
try {
// Test for existence of app with this numerical ID,
// and return typed id if found
return (await sdk.models.application.get(Number(nameOrSlugOrId))).id;
} catch (e) {
const { instanceOf } = await import('../errors');
const { BalenaApplicationNotFound } = await import('balena-errors');
if (!instanceOf(e, BalenaApplicationNotFound)) {
throw e;
}
}
// App with this numerical id not found
// return the passed identifier unchanged
return nameOrSlugOrId;
}
/**
* Wraps the sdk organization.getAll method,
* restricting to those orgs user is a member of
*/
export async function getOwnOrganizations(
sdk: BalenaSDK,
): Promise<Organization[]> {
return await sdk.models.organization.getAll({
$filter: {
organization_membership: {
$any: {
$alias: 'orm',
$expr: {
orm: {
user: await sdk.auth.getUserId(),
},
},
},
},
},
$orderby: 'name asc',
});
}

View File

@ -162,6 +162,7 @@ function sshErrorMessage(exitSignal?: string, exitCode?: number) {
msg.push(`
Are the SSH keys correctly configured in balenaCloud? See:
https://www.balena.io/docs/learn/manage/ssh-access/#add-an-ssh-key-to-balenacloud`);
msg.push('Are you accidentally using `sudo`?');
}
}
return msg.join('\n');

View File

@ -15,10 +15,14 @@ limitations under the License.
*/
import type { BalenaSDK } from 'balena-sdk';
import { Socket } from 'net';
import * as tls from 'tls';
import { TypedError } from 'typed-error';
import { ExpectedError } from '../errors';
const PROXY_CONNECT_TIMEOUT_MS = 10000;
class TunnelServerNotTrustedError extends ExpectedError {}
class UnableToConnectError extends TypedError {
public status: string;
public statusCode: string;
@ -42,17 +46,17 @@ export const tunnelConnectionToDevice = (
sdk: BalenaSDK,
) => {
return Promise.all([
sdk.settings.get('vpnUrl'),
sdk.settings.get('tunnelUrl'),
sdk.auth.whoami(),
sdk.auth.getToken(),
]).then(([vpnUrl, whoami, token]) => {
]).then(([tunnelUrl, whoami, token]) => {
const auth = {
user: whoami || 'root',
password: token,
};
return (client: Socket): Promise<void> =>
openPortThroughProxy(vpnUrl, 3128, auth, uuid, port)
openPortThroughProxy(tunnelUrl, 443, auth, uuid, port)
.then((remote) => {
client.pipe(remote);
remote.pipe(client);
@ -96,30 +100,41 @@ const openPortThroughProxy = (
}
return new Promise<Socket>((resolve, reject) => {
const proxyTunnel = new Socket();
proxyTunnel.on('error', reject);
proxyTunnel.connect(proxyPort, proxyServer, () => {
const proxyConnectionHandler = (data: Buffer) => {
proxyTunnel.removeListener('data', proxyConnectionHandler);
const [httpStatus] = data.toString('utf8').split('\r\n');
const [, httpStatusCode, ...httpMessage] = httpStatus.split(' ');
if (parseInt(httpStatusCode, 10) === 200) {
proxyTunnel.setTimeout(0);
resolve(proxyTunnel);
} else {
const proxyTunnel = tls.connect(
proxyPort,
proxyServer,
{ servername: proxyServer }, // send the hostname in the SNI field
() => {
if (!proxyTunnel.authorized) {
console.error('Unable to authorize the tunnel server');
reject(
new UnableToConnectError(httpStatusCode, httpMessage.join(' ')),
new TunnelServerNotTrustedError(proxyTunnel.authorizationError),
);
return;
}
};
proxyTunnel.on('timeout', () => {
reject(new RemoteSocketNotListening(devicePort));
});
proxyTunnel.on('data', proxyConnectionHandler);
proxyTunnel.setTimeout(PROXY_CONNECT_TIMEOUT_MS);
proxyTunnel.write(httpHeaders.join('\r\n').concat('\r\n\r\n'));
});
proxyTunnel.once('data', (data: Buffer) => {
const [httpStatus] = data.toString('utf8').split('\r\n');
const [, httpStatusCode, ...httpMessage] = httpStatus.split(' ');
if (parseInt(httpStatusCode, 10) === 200) {
proxyTunnel.setTimeout(0);
resolve(proxyTunnel);
} else {
reject(
new UnableToConnectError(httpStatusCode, httpMessage.join(' ')),
);
}
});
proxyTunnel.on('timeout', () => {
reject(new RemoteSocketNotListening(devicePort));
});
proxyTunnel.setTimeout(PROXY_CONNECT_TIMEOUT_MS);
proxyTunnel.write(httpHeaders.join('\r\n').concat('\r\n\r\n'));
},
);
proxyTunnel.on('error', reject);
});
};

View File

@ -117,3 +117,8 @@ export function parseAsLocalHostnameOrIp(input: string, paramName?: string) {
return input;
}
export function looksLikeAppSlug(input: string) {
// One or more non whitespace chars, /, 4 or more non whitespace chars
return /[\S]+\/[\S]{4,}/.test(input);
}

View File

@ -22,13 +22,12 @@ export function isVersionGTE(v: string): boolean {
return semver.gte(process.env.BALENA_CLI_VERSION_OVERRIDE || version, v);
}
let v12: boolean;
let v13: boolean;
export function isV12(): boolean {
if (v12 === undefined) {
// This is the `Change-type: major` PR that will produce v12.0.0.
// Enable the v12 feature switches and run all v12 tests.
v12 = true; // v12 = isVersionGTE('12.0.0');
/** Feature switch for the next major version of the CLI */
export function isV13(): boolean {
if (v13 === undefined) {
v13 = isVersionGTE('13.0.0');
}
return v12;
return v13;
}

2772
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "12.29.0",
"version": "12.44.11",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -84,7 +84,8 @@
"author": "Juan Cruz Viotti <juan@balena.io>",
"license": "Apache-2.0",
"engines": {
"node": ">=10.20.0"
"node": ">=10.20.0 <13.0.0",
"npm": "<7.0.0"
},
"husky": {
"hooks": {
@ -101,7 +102,7 @@
},
"macos": {
"identifier": "io.balena.cli",
"sign": "Developer ID Installer: Rulemotion Ltd (66H43P8FRG)"
"sign": "Developer ID Installer: Balena Ltd (66H43P8FRG)"
},
"plugins": [
"@oclif/plugin-help"
@ -110,7 +111,7 @@
"devDependencies": {
"@balena/lint": "^5.2.0",
"@oclif/config": "^1.17.0",
"@oclif/dev-cli": "^1.22.2",
"@oclif/dev-cli": "^1.26.0",
"@oclif/parser": "^3.8.5",
"@octokit/plugin-throttling": "^3.3.0",
"@octokit/rest": "^16.43.2",
@ -130,7 +131,7 @@
"@types/http-proxy": "^1.17.4",
"@types/intercept-stdout": "^0.1.0",
"@types/is-root": "^2.1.2",
"@types/js-yaml": "^3.12.5",
"@types/js-yaml": "^4.0.0",
"@types/jsonwebtoken": "^8.5.0",
"@types/klaw": "^3.0.1",
"@types/lodash": "^4.14.159",
@ -138,6 +139,7 @@
"@types/mocha": "^8.0.4",
"@types/mock-require": "^2.0.0",
"@types/moment-duration-format": "^2.2.2",
"@types/ndjson": "^2.0.0",
"@types/net-keepalive": "^0.4.1",
"@types/nock": "^11.0.7",
"@types/node": "^10.17.28",
@ -183,7 +185,8 @@
"simple-git": "^1.132.0",
"sinon": "^9.2.1",
"ts-node": "^8.10.2",
"typescript": "^4.0.2"
"typescript": "^4.0.2",
"electron-notarize": "^1.0.0"
},
"dependencies": {
"@balena/dockerignore": "^1.0.2",
@ -195,16 +198,17 @@
"@types/update-notifier": "^4.1.1",
"@zeit/dockerignore": "0.0.3",
"JSONStream": "^1.0.3",
"balena-config-json": "^4.1.0",
"balena-device-init": "^5.0.2",
"balena-errors": "^4.4.1",
"balena-config-json": "^4.1.1",
"balena-device-init": "^6.0.0",
"balena-errors": "^4.7.1",
"balena-image-fs": "^7.0.6",
"balena-image-manager": "^7.0.3",
"balena-preload": "^10.3.1",
"balena-preload": "^10.4.1",
"balena-release": "^3.0.0",
"balena-sdk": "^15.6.0",
"balena-sdk": "^15.31.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.5",
"balena-settings-storage": "^6.0.1",
"balena-settings-client": "^4.0.6",
"balena-settings-storage": "^7.0.0",
"balena-sync": "^11.0.2",
"bluebird": "^3.7.2",
"body-parser": "^1.19.0",
@ -221,7 +225,7 @@
"docker-toolbelt": "^3.3.8",
"dockerode": "^2.5.8",
"ejs": "^3.1.3",
"etcher-sdk": "^2.0.20",
"etcher-sdk": "^6.2.1",
"event-stream": "3.3.4",
"express": "^4.13.3",
"fast-boot2": "^1.1.0",
@ -234,7 +238,7 @@
"inquirer": "^7.3.3",
"is-elevated": "^3.0.0",
"is-root": "^2.1.0",
"js-yaml": "^3.14.0",
"js-yaml": "^4.0.0",
"klaw": "^3.0.0",
"livepush": "^3.5.0",
"lodash": "^4.17.20",
@ -242,20 +246,20 @@
"mixpanel": "^0.10.3",
"moment": "^2.27.0",
"moment-duration-format": "^2.3.2",
"ndjson": "^2.0.0",
"node-cleanup": "^2.1.2",
"node-unzip-2": "^0.2.8",
"oclif": "^1.16.1",
"open": "^7.1.0",
"patch-package": "6.2.2",
"patch-package": "^6.4.7",
"prettyjson": "^1.1.3",
"progress-stream": "^2.0.0",
"reconfix": "^0.1.0",
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
"request": "^2.88.2",
"resin-cli-form": "^2.0.2",
"resin-cli-visuals": "^1.7.0",
"resin-compose-parse": "^2.1.2",
"resin-cli-visuals": "^1.8.0",
"resin-compose-parse": "^2.1.3",
"resin-doodles": "^0.1.1",
"resin-image-fs": "^5.0.9",
"resin-multibuild": "^4.7.2",
"resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2",
@ -276,7 +280,7 @@
"window-size": "^1.1.0"
},
"optionalDependencies": {
"net-keepalive": "^1.3.6",
"net-keepalive": "^2.0.3",
"windosu": "^0.3.0"
}
}

View File

@ -1,310 +0,0 @@
diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js b/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
index cd771cd..4a66939 100644
--- a/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
+++ b/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
@@ -37,6 +37,7 @@ class PackMacos extends command_1.Command {
if (process.env.OSX_KEYCHAIN)
args.push('--keychain', process.env.OSX_KEYCHAIN);
args.push(dist);
+ console.log(`pkgbuild "${args.join('" "')}"`);
await qq.x('pkgbuild', args);
}
}
diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
index a9d4276..4ac508f 100644
--- a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
+++ b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
@@ -3,11 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
const command_1 = require("@oclif/command");
const qq = require("qqjs");
const Tarballs = require("../../tarballs");
+const { fixPath } = require("../../util");
+
class PackWin extends command_1.Command {
async run() {
await this.checkForNSIS();
const { flags } = this.parse(PackWin);
- const buildConfig = await Tarballs.buildConfig(flags.root);
+ const targets = flags.targets !== undefined ? flags.targets.split(',') : undefined;
+ const buildConfig = await Tarballs.buildConfig(flags.root, {targets});
const { config } = buildConfig;
await Tarballs.build(buildConfig, { platform: 'win32', pack: false });
const arches = buildConfig.targets.filter(t => t.platform === 'win32').map(t => t.arch);
@@ -17,7 +20,7 @@ class PackWin extends command_1.Command {
await qq.write([installerBase, `bin/${config.bin}`], scripts.sh(config));
await qq.write([installerBase, `${config.bin}.nsi`], scripts.nsis(config, arch));
await qq.mv(buildConfig.workspace({ platform: 'win32', arch }), [installerBase, 'client']);
- await qq.x(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
+ await qq.x(`makensis ${fixPath(installerBase)}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`)
const o = buildConfig.dist(`win/${config.bin}-v${buildConfig.version}-${arch}.exe`);
await qq.mv([installerBase, 'installer.exe'], o);
this.log(`built ${o}`);
@@ -40,6 +43,7 @@ class PackWin extends command_1.Command {
PackWin.description = 'create windows installer from oclif CLI';
PackWin.flags = {
root: command_1.flags.string({ char: 'r', description: 'path to oclif CLI root', default: '.', required: true }),
+ targets: command_1.flags.string({char: 't', description: 'comma-separated targets to pack (e.g.: win32-x86,win32-x64)'}),
};
exports.default = PackWin;
const scripts = {
@@ -89,6 +93,13 @@ VIAddVersionKey /LANG=\${LANG_ENGLISH} "ProductVersion" "\${VERSION}.0"
InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
Section "${config.name} CLI \${VERSION}"
+ ; First remove any old client files.
+ ; (Remnants of old versions were causing CLI errors)
+ ; Initially tried running the Uninstall.exe, but was
+ ; unable to make script wait for completion (despite using _?)
+ DetailPrint "Removing files from previous version."
+ RMDir /r "$INSTDIR\\client"
+
SetOutPath $INSTDIR
File /r bin
File /r client
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/build.js b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
index 3e613e0..4059ff4 100644
--- a/node_modules/@oclif/dev-cli/lib/tarballs/build.js
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
@@ -17,8 +17,11 @@ const pack = async (from, to) => {
qq.cd(prevCwd);
};
async function build(c, options = {}) {
- const { xz, config } = c;
+ const { xz, config, tmp } = c;
const prevCwd = qq.cwd();
+
+ console.log(`[patched @oclif/dev-cli] cwd="${prevCwd}"\n c.root="${c.root}" c.workspace()="${c.workspace()}"`);
+
const packCLI = async () => {
const stdout = await qq.x.stdout('npm', ['pack', '--unsafe-perm'], { cwd: c.root });
return path.join(c.root, stdout.split('\n').pop());
@@ -34,6 +37,44 @@ async function build(c, options = {}) {
await qq.mv(f, '.');
await qq.rm('package', tarball, 'bin/run.cmd');
};
+ const copyCLI = async() => {
+ const ws = c.workspace();
+ await qq.emptyDir(ws);
+ qq.cd(ws);
+ const sources = [
+ 'CHANGELOG.md',
+ 'INSTALL.md',
+ 'LICENSE',
+ 'README.md',
+ 'TROUBLESHOOTING.md',
+ 'automation/check-npm-version.js',
+ 'bin',
+ 'build',
+ 'npm-shrinkwrap.json',
+ 'package.json',
+ 'patches',
+ 'typings',
+ 'oclif.manifest.json',
+ ];
+ for (const source of sources) {
+ let destDir = ws;
+ const dirname = path.dirname(source);
+ if (dirname && dirname !== '.') {
+ destDir = path.join(ws, dirname);
+ qq.mkdirp(destDir);
+ }
+ console.log(`cp "${source}" -> "${ws}"`);
+ await qq.cp(path.join(c.root, source), destDir);
+ }
+ // rename the original balena-cli ./bin/balena entry point for oclif compatibility
+ await qq.mv('bin/balena', 'bin/run');
+ await qq.rm('bin/run.cmd');
+ // The oclif installers are produced with `npm i --production`, while the
+ // source `bin` folder may contain a `.fast-boot.json` produced with `npm i`.
+ // This has previously led to issues preventing the CLI from starting, so
+ // delete `.fast-boot.json` (if any) from the destination folder.
+ await qq.rm('bin/.fast-boot.json');
+ }
const updatePJSON = async () => {
qq.cd(c.workspace());
const pjson = await qq.readJSON('package.json');
@@ -56,7 +97,13 @@ async function build(c, options = {}) {
lockpath = qq.join(c.root, 'npm-shrinkwrap.json');
}
await qq.cp(lockpath, '.');
- await qq.x('npm install --production');
+
+ const npmVersion = await qq.x.stdout('npm', ['--version']);
+ if (require('semver').lt(npmVersion, '6.9.0')) {
+ await qq.x('npx npm@6.9.0 install --production');
+ } else {
+ await qq.x('npm install --production');
+ }
}
};
const buildTarget = async (target) => {
@@ -71,7 +118,8 @@ async function build(c, options = {}) {
output: path.join(workspace, 'bin', 'node'),
platform: target.platform,
arch: target.arch,
- tmp: qq.join(config.root, 'tmp'),
+ tmp,
+ projectRootPath: c.root,
});
if (options.pack === false)
return;
@@ -124,7 +172,8 @@ async function build(c, options = {}) {
await qq.writeJSON(c.dist(config.s3Key('manifest')), manifest);
};
log_1.log(`gathering workspace for ${config.bin} to ${c.workspace()}`);
- await extractCLI(await packCLI());
+ // await extractCLI(await packCLI());
+ await copyCLI();
await updatePJSON();
await addDependencies();
await bin_1.writeBinScripts({ config, baseWorkspace: c.workspace(), nodeVersion: c.nodeVersion });
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/config.js b/node_modules/@oclif/dev-cli/lib/tarballs/config.js
index 320fc52..efe3f2f 100644
--- a/node_modules/@oclif/dev-cli/lib/tarballs/config.js
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/config.js
@@ -10,7 +10,13 @@ function gitSha(cwd, options = {}) {
}
exports.gitSha = gitSha;
async function Tmp(config) {
- const tmp = path.join(config.root, 'tmp');
+ let tmp;
+ if (process.env.BUILD_TMP) {
+ tmp = path.join(process.env.BUILD_TMP, 'oclif');
+ } else {
+ tmp = path.join(config.root, 'tmp');
+ }
+ console.log(`@oclif/dev-cli tmp="${tmp}"`);
await qq.mkdirp(tmp);
return tmp;
}
@@ -36,7 +42,7 @@ async function buildConfig(root, options = {}) {
s3Config: updateConfig.s3,
nodeVersion: updateConfig.node.version || process.versions.node,
workspace(target) {
- const base = qq.join(config.root, 'tmp');
+ const base = tmp;
if (target && target.platform)
return qq.join(base, [target.platform, target.arch].join('-'), config.s3Key('baseDir', target));
return qq.join(base, config.s3Key('baseDir', target));
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/node.js b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
index b3918c5..073515a 100644
--- a/node_modules/@oclif/dev-cli/lib/tarballs/node.js
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
@@ -1,28 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const errors_1 = require("@oclif/errors");
+const { isMSYS2 } = require('qqjs');
const path = require("path");
const qq = require("qqjs");
const log_1 = require("../log");
-async function checkFor7Zip() {
- try {
- await qq.x('7z', { stdio: [0, null, 2] });
+const { fixPath } = require("../util");
+let try_install_7zip = true;
+async function checkFor7Zip(projectRootPath) {
+ let zPaths = [
+ fixPath(path.join(projectRootPath, 'node_modules', '7zip', '7zip-lite', '7z.exe')),
+ '7z',
+ ];
+ let foundPath = '';
+ for (const zPath of zPaths) {
+ try {
+ console.log(`probing 7zip at "${zPath}"...`);
+ await qq.x(zPath, { stdio: [0, null, 2] });
+ foundPath = zPath;
+ break;
+ }
+ catch (err) {}
}
- catch (err) {
- if (err.code === 127)
- errors_1.error('install 7-zip to package windows tarball');
- else
- throw err;
+ if (foundPath) {
+ console.log(`found 7zip at "${foundPath}"`);
+ } else if (try_install_7zip) {
+ try_install_7zip = false;
+ console.log(`attempting "npm install 7zip"...`);
+ qq.pushd(projectRootPath);
+ try {
+ await qq.x('npm', ['install', '--no-save', '7zip']);
+ } catch (err) {
+ errors_1.error('install 7-zip to package windows tarball', true);
+ } finally {
+ qq.popd();
+ }
+ return checkFor7Zip(projectRootPath);
+ } else {
+ errors_1.error('install 7-zip to package windows tarball', true);
}
+ return foundPath;
}
-async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
+async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp, projectRootPath }) {
+
+ console.log(`fetchNodeBinary using tmp="${tmp}`);
+
if (arch === 'arm')
arch = 'armv7l';
let nodeBase = `node-v${nodeVersion}-${platform}-${arch}`;
let tarball = path.join(tmp, 'node', `${nodeBase}.tar.xz`);
let url = `https://nodejs.org/dist/v${nodeVersion}/${nodeBase}.tar.xz`;
+ let zPath = '';
if (platform === 'win32') {
- await checkFor7Zip();
+ zPath = await checkFor7Zip(projectRootPath);
nodeBase = `node-v${nodeVersion}-win-${arch}`;
tarball = path.join(tmp, 'node', `${nodeBase}.7z`);
url = `https://nodejs.org/dist/v${nodeVersion}/${nodeBase}.7z`;
@@ -40,7 +70,8 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
const basedir = path.dirname(tarball);
await qq.mkdirp(basedir);
await qq.download(url, tarball);
- await qq.x(`grep ${path.basename(tarball)} ${shasums} | shasum -a 256 -c -`, { cwd: basedir });
+ const shaCmd = isMSYS2 ? 'sha256sum -c -' : 'shasum -a 256 -c -';
+ await qq.x(`grep ${path.basename(tarball)} ${fixPath(shasums)} | ${shaCmd}`, { cwd: basedir });
};
const extract = async () => {
log_1.log(`extracting ${nodeBase}`);
@@ -50,7 +81,7 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
await qq.mkdirp(path.dirname(cache));
if (platform === 'win32') {
qq.pushd(nodeTmp);
- await qq.x(`7z x -bd -y ${tarball} > /dev/null`);
+ await qq.x(`"${zPath}" x -bd -y ${fixPath(tarball)} > /dev/null`);
await qq.mv([nodeBase, 'node.exe'], cache);
qq.popd();
}
diff --git a/node_modules/@oclif/dev-cli/lib/util.js b/node_modules/@oclif/dev-cli/lib/util.js
index 17368b4..9d3fcf9 100644
--- a/node_modules/@oclif/dev-cli/lib/util.js
+++ b/node_modules/@oclif/dev-cli/lib/util.js
@@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("lodash");
+const { isCygwin, isMinGW, isMSYS2 } = require('qqjs');
function castArray(input) {
if (input === undefined)
return [];
@@ -40,3 +41,17 @@ function sortBy(arr, fn) {
}
exports.sortBy = sortBy;
exports.template = (context) => (t) => _.template(t || '')(context);
+
+function fixPath(badPath) {
+ console.log(`fixPath MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
+ // 'c:\myfolder' -> '/c/myfolder' or '/cygdrive/c/myfolder'
+ let fixed = badPath.replace(/\\/g, '/');
+ if (isMSYS2 || isMinGW) {
+ fixed = fixed.replace(/^([a-zA-Z]):/, '/$1');
+ } else if (isCygwin) {
+ fixed = fixed.replace(/^([a-zA-Z]):/, '/cygdrive/$1');
+ }
+ console.log(`[patched @oclif/dev-cli] fixPath before="${badPath}" after="${fixed}"`);
+ return fixed;
+}
+exports.fixPath = fixPath;

View File

@ -0,0 +1,250 @@
diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js b/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
index e0abbbe..debf799 100644
--- a/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
+++ b/node_modules/@oclif/dev-cli/lib/commands/pack/macos.js
@@ -128,6 +128,7 @@ class PackMacos extends command_1.Command {
if (process.env.OSX_KEYCHAIN)
args.push('--keychain', process.env.OSX_KEYCHAIN);
args.push(dist);
+ console.error(`[debug] @oclif/dev-cli pkgbuild "${args.join('" "')}"`);
await qq.x('pkgbuild', args);
}
}
diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
index a313991..6681892 100644
--- a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
+++ b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
@@ -51,6 +51,13 @@ VIAddVersionKey /LANG=\${LANG_ENGLISH} "ProductVersion" "\${VERSION}.0"
InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
Section "${config.name} CLI \${VERSION}"
+ ; First remove any old client files.
+ ; (Remnants of old versions were causing CLI errors)
+ ; Initially tried running the Uninstall.exe, but was
+ ; unable to make script wait for completion (despite using _?)
+ DetailPrint "Removing files from previous version."
+ RMDir /r "$INSTDIR\\client"
+
SetOutPath $INSTDIR
File /r bin
File /r client
@@ -192,7 +199,8 @@ class PackWin extends command_1.Command {
async run() {
await this.checkForNSIS();
const { flags } = this.parse(PackWin);
- const buildConfig = await Tarballs.buildConfig(flags.root);
+ const targets = flags.targets ? flags.targets.split(',') : undefined;
+ const buildConfig = await Tarballs.buildConfig(flags.root, { targets });
const { config } = buildConfig;
await Tarballs.build(buildConfig, { platform: 'win32', pack: false });
const arches = buildConfig.targets.filter(t => t.platform === 'win32').map(t => t.arch);
@@ -207,7 +215,8 @@ class PackWin extends command_1.Command {
// eslint-disable-next-line no-await-in-loop
await qq.mv(buildConfig.workspace({ platform: 'win32', arch }), [installerBase, 'client']);
// eslint-disable-next-line no-await-in-loop
- await qq.x(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
+ const { msysExec, toMsysPath } = require("../../util");
+ await msysExec(`makensis ${toMsysPath(installerBase)}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
const o = buildConfig.dist(`win/${config.bin}-v${buildConfig.version}-${arch}.exe`);
// eslint-disable-next-line no-await-in-loop
await qq.mv([installerBase, 'installer.exe'], o);
@@ -232,4 +241,5 @@ exports.default = PackWin;
PackWin.description = 'create windows installer from oclif CLI';
PackWin.flags = {
root: command_1.flags.string({ char: 'r', description: 'path to oclif CLI root', default: '.', required: true }),
+ targets: command_1.flags.string({char: 't', description: 'comma-separated targets to pack (e.g.: win32-x86,win32-x64)'}),
};
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/build.js b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
index c6bd245..baa7f6f 100644
--- a/node_modules/@oclif/dev-cli/lib/tarballs/build.js
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
@@ -18,8 +18,9 @@ const pack = async (from, to) => {
qq.cd(prevCwd);
};
async function build(c, options = {}) {
- const { xz, config } = c;
+ const { xz, config, tmp } = c;
const prevCwd = qq.cwd();
+ console.error(`[debug] @oclif/dev-cli cwd="${prevCwd}"\n c.root="${c.root}" c.workspace()="${c.workspace()}"`);
const packCLI = async () => {
const stdout = await qq.x.stdout('npm', ['pack', '--unsafe-perm'], { cwd: c.root });
return path.join(c.root, stdout.split('\n').pop());
@@ -30,11 +31,19 @@ async function build(c, options = {}) {
tarball = path.basename(tarball);
tarball = qq.join([c.workspace(), tarball]);
qq.cd(c.workspace());
- await qq.x(`tar -xzf ${tarball}`);
+ const { msysExec, toMsysPath } = require("../util");
+ await msysExec(`tar -xzf ${toMsysPath(tarball)}`);
// eslint-disable-next-line no-await-in-loop
for (const f of await qq.ls('package', { fullpath: true }))
await qq.mv(f, '.');
await qq.rm('package', tarball, 'bin/run.cmd');
+ // rename the original balena-cli ./bin/balena entry point for oclif compatibility
+ await qq.mv('bin/balena', 'bin/run');
+ // The oclif installers are a production installation, while the source
+ // `bin` folder may contain a `.fast-boot.json` file of a dev installation.
+ // This has previously led to issues preventing the CLI from starting, so
+ // delete `.fast-boot.json` (if any) from the destination folder.
+ await qq.rm('bin/.fast-boot.json');
};
const updatePJSON = async () => {
qq.cd(c.workspace());
@@ -46,21 +55,21 @@ async function build(c, options = {}) {
await qq.writeJSON('package.json', pjson);
};
const addDependencies = async () => {
- qq.cd(c.workspace());
- const yarnRoot = findYarnWorkspaceRoot(c.root) || c.root;
- const yarn = await qq.exists([yarnRoot, 'yarn.lock']);
- if (yarn) {
- await qq.cp([yarnRoot, 'yarn.lock'], '.');
- await qq.x('yarn --no-progress --production --non-interactive');
- }
- else {
- let lockpath = qq.join(c.root, 'package-lock.json');
- if (!await qq.exists(lockpath)) {
- lockpath = qq.join(c.root, 'npm-shrinkwrap.json');
- }
- await qq.cp(lockpath, '.');
- await qq.x('npm install --production');
+ const ws = c.workspace();
+ qq.cd(ws);
+ console.error(`[debug] @oclif/dev-cli copying node_modules to "${ws}"`)
+ const source = path.join(c.root, 'node_modules');
+ if (process.platform === 'win32') {
+ // xcopy is much faster than `qq.cp(source, ws)`
+ await qq.x(`xcopy "${source}" "${ws}\\node_modules" /S /E /B /I /K /Q /Y`);
+ } else {
+ // use the shell's `cp` on macOS in order to preserve extended
+ // file attributes containing `codesign` digital signatures
+ await qq.x(`cp -pR "${source}" "${ws}"`);
}
+ console.error(`[debug] @oclif/dev-cli running "npm prune --production" in "${ws}"`);
+ await qq.x('npm prune --production');
+ console.error(`[debug] @oclif/dev-cli done`);
};
const buildTarget = async (target) => {
const workspace = c.workspace(target);
@@ -74,7 +83,8 @@ async function build(c, options = {}) {
output: path.join(workspace, 'bin', 'node'),
platform: target.platform,
arch: target.arch,
- tmp: qq.join(config.root, 'tmp'),
+ tmp,
+ projectRootPath: c.root,
});
if (options.pack === false)
return;
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/config.js b/node_modules/@oclif/dev-cli/lib/tarballs/config.js
index 9754a6b..68ef6b7 100644
--- a/node_modules/@oclif/dev-cli/lib/tarballs/config.js
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/config.js
@@ -17,7 +17,10 @@ function gitSha(cwd, options = {}) {
}
exports.gitSha = gitSha;
async function Tmp(config) {
- const tmp = path.join(config.root, 'tmp');
+ const tmp = process.env.BUILD_TMP
+ ? path.join(process.env.BUILD_TMP, 'oclif')
+ : path.join(config.root, 'tmp');
+ console.error(`[debug] @oclif/dev-cli tmp="${tmp}"`);
await qq.mkdirp(tmp);
return tmp;
}
@@ -44,7 +47,7 @@ async function buildConfig(root, options = {}) {
s3Config: updateConfig.s3,
nodeVersion: updateConfig.node.version || process.versions.node,
workspace(target) {
- const base = qq.join(config.root, 'tmp');
+ const base = tmp;
if (target && target.platform)
return qq.join(base, [target.platform, target.arch].join('-'), config.s3Key('baseDir', target));
return qq.join(base, config.s3Key('baseDir', target));
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/node.js b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
index fabe5c4..e32dd76 100644
--- a/node_modules/@oclif/dev-cli/lib/tarballs/node.js
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
@@ -4,9 +4,10 @@ const errors_1 = require("@oclif/errors");
const path = require("path");
const qq = require("qqjs");
const log_1 = require("../log");
+const { isMSYS2, msysExec, toMsysPath } = require("../util");
async function checkFor7Zip() {
try {
- await qq.x('7z', { stdio: [0, null, 2] });
+ await msysExec('7z', { stdio: [0, null, 2] });
}
catch (error) {
if (error.code === 127)
@@ -41,7 +42,8 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
const basedir = path.dirname(tarball);
await qq.mkdirp(basedir);
await qq.download(url, tarball);
- await qq.x(`grep ${path.basename(tarball)} ${shasums} | shasum -a 256 -c -`, { cwd: basedir });
+ const shaCmd = isMSYS2 ? 'sha256sum -c -' : 'shasum -a 256 -c -';
+ await msysExec(`grep ${path.basename(tarball)} ${toMsysPath(shasums)} | ${shaCmd}`, { cwd: basedir });
};
const extract = async () => {
log_1.log(`extracting ${nodeBase}`);
@@ -51,7 +53,7 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
await qq.mkdirp(path.dirname(cache));
if (platform === 'win32') {
qq.pushd(nodeTmp);
- await qq.x(`7z x -bd -y ${tarball} > /dev/null`);
+ await msysExec(`7z x -bd -y ${toMsysPath(tarball)} > /dev/null`);
await qq.mv([nodeBase, 'node.exe'], cache);
qq.popd();
}
diff --git a/node_modules/@oclif/dev-cli/lib/util.js b/node_modules/@oclif/dev-cli/lib/util.js
index b3d48b7..540bbe6 100644
--- a/node_modules/@oclif/dev-cli/lib/util.js
+++ b/node_modules/@oclif/dev-cli/lib/util.js
@@ -40,3 +40,47 @@ function sortBy(arr, fn) {
}
exports.sortBy = sortBy;
exports.template = (context) => (t) => _.template(t || '')(context);
+
+// OSTYPE is 'msys' for MSYS 1.0 and for MSYS2, or 'cygwin' for Cygwin
+// but note that OSTYPE is not "exported" by default, so run: export OSTYPE=$OSTYPE
+// MSYSTEM is 'MINGW32' for MSYS 1.0, 'MSYS' for MSYS2, and undefined for Cygwin
+const isCygwin = process.env.OSTYPE === 'cygwin';
+const isMinGW = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MINGW');
+const isMSYS2 = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MSYS');
+const MSYSSHELLPATH = process.env.MSYSSHELLPATH ||
+ (isMSYS2 ? 'C:\\msys64\\usr\\bin\\bash.exe' :
+ (isMinGW ? 'C:\\MinGW\\msys\\1.0\\bin\\bash.exe' :
+ (isCygwin ? 'C:\\cygwin64\\bin\\bash.exe' : '/bin/sh')));
+
+exports.isCygwin = isCygwin;
+exports.isMinGW = isMinGW;
+exports.isMSYS2 = isMSYS2;
+console.error(`[debug] @oclif/dev-cli MSYSSHELLPATH=${MSYSSHELLPATH} MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
+
+const qq = require("qqjs");
+
+/* Convert a Windows path like 'C:\tmp' to a MSYS path like '/c/tmp' */
+function toMsysPath(windowsPath) {
+ // 'c:\myfolder' -> '/c/myfolder' or '/cygdrive/c/myfolder'
+ let msysPath = windowsPath.replace(/\\/g, '/');
+ if (isMSYS2 || isMinGW) {
+ msysPath = msysPath.replace(/^([a-zA-Z]):/, '/$1');
+ } else if (isCygwin) {
+ msysPath = msysPath.replace(/^([a-zA-Z]):/, '/cygdrive/$1');
+ }
+ console.error(`[debug] @oclif/dev-cli toMsysPath before="${windowsPath}" after="${msysPath}"`);
+ return msysPath;
+}
+exports.toMsysPath = toMsysPath;
+
+/* Like qqjs qq.x(), but using MSYS bash on Windows instead of cmd.exe */
+async function msysExec(cmd, options = {}) {
+ if (process.platform !== 'win32') {
+ return qq.x(cmd, options);
+ }
+ const sh = MSYSSHELLPATH;
+ const args = ['-c', cmd];
+ console.error(`[debug] @oclif/dev-cli msysExec sh="${sh}" args=${JSON.stringify(args)} options=${JSON.stringify(options)}`);
+ return qq.x(sh, args, options);
+}
+exports.msysExec = msysExec;

View File

@ -0,0 +1,17 @@
diff --git a/node_modules/exit-hook/index.js b/node_modules/exit-hook/index.js
index e18013f..3366356 100644
--- a/node_modules/exit-hook/index.js
+++ b/node_modules/exit-hook/index.js
@@ -14,9 +14,9 @@ function exit(exit, signal) {
el();
});
- if (exit === true) {
- process.exit(128 + signal);
- }
+ // if (exit === true) {
+ // process.exit(128 + signal);
+ // }
};
module.exports = function (cb) {

View File

@ -1,64 +0,0 @@
diff --git a/node_modules/qqjs/node_modules/execa/index.js b/node_modules/qqjs/node_modules/execa/index.js
index 06f3969..8bca191 100644
--- a/node_modules/qqjs/node_modules/execa/index.js
+++ b/node_modules/qqjs/node_modules/execa/index.js
@@ -14,6 +14,17 @@ const stdio = require('./lib/stdio');
const TEN_MEGABYTES = 1000 * 1000 * 10;
+// OSTYPE is 'msys' for MSYS 1.0 and for MSYS2, or 'cygwin' for Cygwin
+// but note that OSTYPE is not "exported" by default, so run: export OSTYPE=$OSTYPE
+// MSYSTEM is 'MINGW32' for MSYS 1.0, 'MSYS' for MSYS2, and undefined for Cygwin
+const isCygwin = process.env.OSTYPE === 'cygwin';
+const isMinGW = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MINGW');
+const isMSYS2 = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MSYS');
+
+console.log(`[patched execa] detected "${
+ isCygwin ? 'Cygwin' : isMinGW ? 'MinGW' : isMSYS2 ? 'MSYS2' : 'standard'
+}" environment (MSYSTEM="${process.env.MSYSTEM}")`);
+
function handleArgs(cmd, args, opts) {
let parsed;
@@ -104,13 +115,22 @@ function handleShell(fn, cmd, opts) {
opts = Object.assign({}, opts);
- if (process.platform === 'win32') {
+ if (isMSYS2 || isMinGW || isCygwin) {
+ file = process.env.MSYSSHELLPATH ||
+ (isMSYS2 ? 'C:\\msys64\\usr\\bin\\bash.exe' :
+ (isMinGW ? 'C:\\MinGW\\msys\\1.0\\bin\\bash.exe' :
+ (isCygwin ? 'C:\\cygwin64\\bin\\bash.exe' : file)));
+ }
+ else if (process.platform === 'win32') {
opts.__winShell = true;
file = process.env.comspec || 'cmd.exe';
args = ['/s', '/c', `"${cmd}"`];
opts.windowsVerbatimArguments = true;
}
+ const argStr = (args && args.length) ? `["${args.join('", "')}"]` : args;
+ console.log(`[patched execa] handleShell file="${file}" args=${argStr}`);
+
if (opts.shell) {
file = opts.shell;
delete opts.shell;
@@ -199,6 +219,9 @@ module.exports = (cmd, args, opts) => {
const maxBuffer = parsed.opts.maxBuffer;
const joinedCmd = joinCmd(cmd, args);
+ const argStr = (args && args.length) ? `["${args.join('", "')}"]` : args;
+ console.log(`[patched execa] parsed.cmd="${parsed.cmd}" parsed.args=${argStr}`);
+
let spawned;
try {
spawned = childProcess.spawn(parsed.cmd, parsed.args, parsed.opts);
@@ -364,3 +387,7 @@ module.exports.sync = (cmd, args, opts) => {
module.exports.shellSync = (cmd, opts) => handleShell(module.exports.sync, cmd, opts);
module.exports.spawn = util.deprecate(module.exports, 'execa.spawn() is deprecated. Use execa() instead.');
+
+module.exports.isCygwin = isCygwin;
+module.exports.isMinGW = isMinGW;
+module.exports.isMSYS2 = isMSYS2;

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