mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
207 Commits
fix-window
...
test-appim
Author | SHA1 | Date | |
---|---|---|---|
fed4328dec | |||
b4495839ca | |||
f45ac42dd3 | |||
fa26004648 | |||
ba1ea54d69 | |||
9fb62d92b7 | |||
8780a24fb5 | |||
3d3e91d49d | |||
f6e6d9ce8b | |||
0f9d78ab50 | |||
06f7683837 | |||
83a23d9f30 | |||
ffa181a2c3 | |||
d50d18d492 | |||
0b0fb94834 | |||
c1244c0c98 | |||
213e54feb1 | |||
cc8a8513e9 | |||
42c3236313 | |||
91fd515266 | |||
57cd096612 | |||
854501cf8d | |||
d44afa8c39 | |||
b7500fc2c2 | |||
dc6c8d7472 | |||
5c5be8f7b7 | |||
5bdd6c6034 | |||
a5bade99fc | |||
9c3eb76856 | |||
973f1a9c40 | |||
16ea0c9d6d | |||
73bfe545e8 | |||
f53e658ca2 | |||
b66706e8ee | |||
11e50466d5 | |||
431c4b6e4a | |||
d12490f816 | |||
67b7b8b5d0 | |||
16d1f0f06f | |||
9676ea94cb | |||
df8ce0bbe0 | |||
6437bb7511 | |||
ac96616e4e | |||
2737c9c53c | |||
3b8a46f523 | |||
3ac1994941 | |||
b3a6c6cb0f | |||
6d4faa7b2c | |||
9036ce9af3 | |||
4911db640f | |||
e7999f52a9 | |||
68b61e7424 | |||
329b84d01e | |||
25b1dff5d8 | |||
fb1768b4ca | |||
cbc1e52256 | |||
37c2880996 | |||
835445be2e | |||
52fe7481fc | |||
88072173d0 | |||
fdc2bff063 | |||
4f6f20f469 | |||
50af0760ce | |||
43906d22c8 | |||
43f1188f1d | |||
2629a01c7f | |||
5fc009a6ae | |||
480f84993b | |||
d1fdbd927e | |||
4bfd345b68 | |||
d4a153d2ee | |||
3cff091e3a | |||
b2ad9f1643 | |||
f7623bef85 | |||
af63794571 | |||
65d5bdff08 | |||
23165806aa | |||
3649bafbb1 | |||
c62445a399 | |||
b233ea3e3e | |||
4fe660b3a5 | |||
1f07cd1b1c | |||
bcea5193a1 | |||
8b99cd7170 | |||
1986c9339c | |||
b90c9b0d7e | |||
e28c3f9814 | |||
d054ced541 | |||
c8e4d2c9a6 | |||
9671372b9e | |||
2a4ff75203 | |||
f3d750a024 | |||
a701cd8d4d | |||
e2c0c2f359 | |||
15fc805f89 | |||
0a995ecc49 | |||
1ba992ada2 | |||
e47fd0c887 | |||
af1de34840 | |||
96fb525378 | |||
3d1f16c0ab | |||
6fb58a25fc | |||
e6b85c9cf8 | |||
43b93e7fd4 | |||
a05dcf08b8 | |||
9636985ee7 | |||
023fc57914 | |||
492bdab2fe | |||
941c365259 | |||
fed58278c9 | |||
d74af38bfe | |||
53926067ca | |||
7181dc5401 | |||
e35e13f9a7 | |||
6e0638f3be | |||
d60ec13d5c | |||
731e50a757 | |||
b363d28664 | |||
7ae83d9ce5 | |||
31281549a6 | |||
e86bcc438c | |||
a1cf602f6f | |||
4cd3ef8b91 | |||
e4eb4586f5 | |||
360c6e42f8 | |||
f76702c4e0 | |||
d3586696b4 | |||
f73e3db4de | |||
1f74889386 | |||
743de66138 | |||
8d56fe9678 | |||
3d9d8bf5c8 | |||
8c3df9ae30 | |||
e71184ed3a | |||
caadce6c2b | |||
f45fac6138 | |||
aeff5997d0 | |||
b5028c65cc | |||
f69276e7c9 | |||
9fff9266d4 | |||
0e7f953f72 | |||
61b11994b5 | |||
1e1935cfb1 | |||
27e2b03702 | |||
358acbd2c8 | |||
b040a21268 | |||
074fe010bd | |||
34557e35ee | |||
3bff569758 | |||
cf06a8dfad | |||
584aa745f7 | |||
194d12cb3d | |||
7739379444 | |||
5c93df921e | |||
da652c6bce | |||
1cd341e6cd | |||
9d2884aab7 | |||
f128eaf389 | |||
70b0524eb6 | |||
c898747468 | |||
6fc3b0df58 | |||
746676beb9 | |||
611f59a0da | |||
c6430274e5 | |||
9637f75617 | |||
439d8391ee | |||
0d3ca63f00 | |||
1f3677bdb2 | |||
10bca728f0 | |||
9763a14e97 | |||
fe24280adf | |||
a11f9ec705 | |||
836ae1cf4a | |||
b4d37e7a3a | |||
055ad834e7 | |||
d2cb88dfb8 | |||
d096743e78 | |||
511d0dbe26 | |||
6b0201866f | |||
9e20b2b691 | |||
665e0cf9d7 | |||
b319ec7281 | |||
ae3ccf759f | |||
309b1ba6a0 | |||
532c4a1862 | |||
fc8b7c71fc | |||
07666e953f | |||
54731c2d20 | |||
d00db5ea8c | |||
5497835728 | |||
5bb05f3a8c | |||
659eda8cd1 | |||
a19132d3bf | |||
140993f554 | |||
575eaf6de1 | |||
3edf7a038f | |||
ad16c5270e | |||
adadefdf3f | |||
19fab40398 | |||
4dc53eb056 | |||
9c96da7515 | |||
8a3e386d21 | |||
5eaa4cfb9f | |||
cb2b90732b | |||
090fc58d10 | |||
3b05971098 | |||
aae6aff3e9 |
37
.dockerignore
Normal file
37
.dockerignore
Normal 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
69
.gitignore
vendored
@ -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/
|
||||
|
@ -15,6 +15,3 @@ npm:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
|
||||
docker:
|
||||
publish: false
|
||||
|
25
.travis.yml
25
.travis.yml
@ -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
445
CHANGELOG.md
445
CHANGELOG.md
@ -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]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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'
|
||||
|
43
appveyor.yml
43
appveyor.yml
@ -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')
|
5
automation/appimage/Dockerfile
Normal file
5
automation/appimage/Dockerfile
Normal 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
|
7
automation/appimage/balena.desktop
Normal file
7
automation/appimage/balena.desktop
Normal 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
BIN
automation/appimage/icon.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
12
automation/appimage/make-appimage.sh
Normal file
12
automation/appimage/make-appimage.sh
Normal 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
|
@ -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`);
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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[];
|
||||
};
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
571
doc/cli.markdown
571
doc/cli.markdown
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
87
lib/commands/device/deactivate.ts
Normal file
87
lib/commands/device/deactivate.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -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: {
|
||||
|
114
lib/commands/device/local-mode.ts
Normal file
114
lib/commands/device/local-mode.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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>';
|
||||
|
||||
|
@ -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;
|
||||
|
16
lib/commands/env/add.ts
vendored
16
lib/commands/env/add.ts
vendored
@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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 || '*';
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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'],
|
||||
|
@ -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
56
lib/commands/orgs.ts
Normal 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']),
|
||||
);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
}),
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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',
|
||||
}),
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
};
|
@ -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,
|
||||
});
|
||||
|
12
lib/utils/compose-types.d.ts
vendored
12
lib/utils/compose-types.d.ts
vendored
@ -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>;
|
||||
}
|
||||
|
||||
|
@ -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} */
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -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,
|
||||
);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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`;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>(
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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',
|
||||
});
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
2772
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
250
patches/all/@oclif+dev-cli+1.26.0.patch
Normal file
250
patches/all/@oclif+dev-cli+1.26.0.patch
Normal 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;
|
17
patches/all/exit-hook+1.1.1.patch
Normal file
17
patches/all/exit-hook+1.1.1.patch
Normal 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) {
|
@ -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
Reference in New Issue
Block a user