mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
147 Commits
fix-window
...
v12.42.0
Author | SHA1 | Date | |
---|---|---|---|
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/
|
||||
|
73
.resinci.yml
73
.resinci.yml
@ -17,4 +17,75 @@ npm:
|
||||
- "14"
|
||||
|
||||
docker:
|
||||
publish: false
|
||||
builds:
|
||||
- path: .
|
||||
dockerfile: ./docker/alpine/Dockerfile
|
||||
docker_repo: balenalib/amd64-alpine-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/amd64-alpine-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/amd64-alpine-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/alpine/Dockerfile
|
||||
docker_repo: balenalib/armv7hf-alpine-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/armv7hf-alpine-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/armv7hf-alpine-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/alpine/Dockerfile
|
||||
docker_repo: balenalib/i386-alpine-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/i386-alpine-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/i386-alpine-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/alpine/Dockerfile
|
||||
docker_repo: balenalib/rpi-alpine-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/rpi-alpine-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/rpi-alpine-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/aarch64-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/aarch64-debian-node:12.19.1-build-20201118
|
||||
- RUN_BASE=balenalib/aarch64-debian-node:12.19.1-run-20201118
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/amd64-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/amd64-debian-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/amd64-debian-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/armv7hf-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/armv7hf-debian-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/armv7hf-debian-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/i386-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/i386-debian-node:12.16.3-build-20200518
|
||||
- RUN_BASE=balenalib/i386-debian-node:12.16.3-build-20200518
|
||||
publish: true
|
||||
|
||||
- path: .
|
||||
dockerfile: ./docker/debian/Dockerfile
|
||||
docker_repo: balenalib/rpi-debian-balenacli
|
||||
args:
|
||||
- BUILD_BASE=balenalib/rpi-debian-node:12.19.1-build-20201211
|
||||
- RUN_BASE=balenalib/rpi-debian-node:12.19.1-run-20201211
|
||||
publish: true
|
||||
|
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
369
CHANGELOG.md
369
CHANGELOG.md
@ -4,6 +4,375 @@ 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.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]
|
||||
|
@ -13,6 +13,8 @@ There are 3 options to choose from to install balena's CLI:
|
||||
Recommended also for scripted installation in CI (continuous integration) environments.
|
||||
* [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested
|
||||
in integrating the balena CLI in their existing projects or workflow.
|
||||
* [Docker Installation](#docker-installation): recommended for users that would like to run the
|
||||
CLI on edge devices or systems where npm installation may not be an option.
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
@ -83,9 +85,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++`
|
||||
@ -148,3 +148,10 @@ To use a remote Docker Engine (daemon) or balenaEngine, specify the remote machi
|
||||
port number with the `--dockerHost` and `--dockerPort` command-line options. For more details,
|
||||
check `balena help build` or the [online
|
||||
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).
|
||||
|
||||
## Docker Installation
|
||||
|
||||
[balenalib images](https://www.balena.io/docs/reference/base-images/base-images/)
|
||||
are available for the balena CLI. They can be used interactively with `docker run`, or
|
||||
as a base image for your application containers. Check out [Docker.md](docker/DOCKER.md)
|
||||
on how to pick an image and get started!
|
||||
|
@ -11,35 +11,52 @@ 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
|
||||
[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 (laptop) as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
@ -29,10 +29,10 @@ and `preload` commands may require additional software to be installed, as descr
|
||||
### 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
|
||||
[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 (laptop) as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
@ -30,10 +30,10 @@ 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
|
||||
[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 (laptop) as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
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')
|
@ -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',
|
||||
|
@ -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
|
||||
|
||||
|
@ -20,7 +20,8 @@ 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 MSYS2_BASH =
|
||||
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
/** Tap and buffer this process' stdout and stderr */
|
||||
|
@ -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
|
||||
|
533
doc/cli.markdown
533
doc/cli.markdown
File diff suppressed because it is too large
Load Diff
410
docker/DOCKER.md
Normal file
410
docker/DOCKER.md
Normal file
@ -0,0 +1,410 @@
|
||||
# Docker Images for the balena CLI
|
||||
|
||||
Docker images with the balena CLI and Docker-in-Docker.
|
||||
|
||||
## Features Overview
|
||||
|
||||
These CLI images are based on the popular [Balena base images](https://www.balena.io/docs/reference/base-images/base-images/)
|
||||
so they include many of the features you see there.
|
||||
|
||||
- Multiple Architectures:
|
||||
- `rpi`
|
||||
- `armv7hf`
|
||||
- `aarch64` (debian only)
|
||||
- `amd64`
|
||||
- `i386`
|
||||
- Multiple Distributions
|
||||
- `debian`
|
||||
- `alpine`
|
||||
- [cross-build](https://www.balena.io/docs/reference/base-images/base-images/#building-arm-containers-on-x86-machines) functionality for building ARM containers on x86.
|
||||
- Helpful package installer script called `install_packages` inspired by [minideb](https://github.com/bitnami/minideb#why-use-minideb).
|
||||
|
||||
Note that there are some additional considerations when running the CLI via Docker so
|
||||
pay close attention to the [Usage](#usage) section for examples of different CLI commands.
|
||||
|
||||
## Image Names
|
||||
|
||||
`balenalib/<arch>-<distro>-balenacli:<cli_ver>`
|
||||
|
||||
- `<arch>` is the architecture and is mandatory. If using `Dockerfile.template`, you can replace this with `%%BALENA_ARCH%%`.
|
||||
For a list of available device names and architectures, see the [Device types](https://www.balena.io/docs/reference/base-images/devicetypes/).
|
||||
- `<distro>` is the Linux distribution and is mandatory. Currently there are 2 distributions, namely `debian` and `alpine`.
|
||||
|
||||
## Image Tags
|
||||
|
||||
In the tags, all of the fields are optional, and if they are left out, they will default to their `latest` pointer.
|
||||
|
||||
- `<cli_ver>` is the version of the balena CLI, for example, `12.40.2`, it can also be substituted for `latest`.
|
||||
|
||||
## Examples
|
||||
|
||||
`balenalib/amd64-debian-balenacli:12.40.2`
|
||||
|
||||
- `<arch>`: amd64 - suitable for running on most workstations
|
||||
- `<distro>`: debian - widely used base distro
|
||||
- `<cli_ver>`: 12.40.2
|
||||
|
||||
`balenalib/armv7hf-alpine-balenacli`
|
||||
|
||||
- `<arch>`: armv7hf - suitable for running on a Raspberry Pi 3 for example
|
||||
- `<distro>`: alpine - smaller footprint than debian
|
||||
- `<cli_ver>`: omitted - the latest available CLI version will be used
|
||||
|
||||
## Volumes
|
||||
|
||||
Volumes can be used to persist data between instances of the CLI container, or to share
|
||||
files between the host and the container.
|
||||
In most cases these are optional, but some examples will highlight when volumes are required.
|
||||
|
||||
- `-v "balena_data:/root/.balena"`: persist balena credentials and downloads between instances
|
||||
- `-v "docker_data:/var/lib/docker"`: persist cache between instances when using Docker-in-Docker (requires `-e "DOCKERD=1"`)
|
||||
- `-v "$PWD:$PWD" -w "$PWD"`: bind mount your current working directory in the container to share app sources or balenaOS image files
|
||||
- `-v "${SSH_AUTH_SOCK}:/ssh-agent"`: bind mount your host ssh-agent socket with preloaded SSH keys
|
||||
- `-v "/var/run/docker.sock:/var/run/docker.sock"`: bind mount your host Docker socket instead of Docker-in-Docker
|
||||
|
||||
## Environment Variables
|
||||
|
||||
These environment variables are available for additional functionality included in the CLI image.
|
||||
In most cases these are optional, but some examples will highlight when environment variables are required.
|
||||
|
||||
- `-e "SSH_PRIVATE_KEY=$(</path/to/priv/key)"`: copy your private SSH key file contents as an environment variable
|
||||
- `-e "DOCKERD=1"`: enable the included Docker-in-Docker daemon (requires `--cap-add SYS_ADMIN`)
|
||||
|
||||
## Keeping the CLI image up to date
|
||||
|
||||
Please note that using the `:latest` tag is not enough to keep the image up to date,
|
||||
because Docker will reuse a locally cached image. To update the image to the latest
|
||||
version, run:
|
||||
|
||||
```bash
|
||||
$ docker pull balenalib/<arch>-<distro>-balenacli
|
||||
```
|
||||
|
||||
Replacing `<arch>` and `<distro>` with the image architecture and distribution as
|
||||
described earlier.
|
||||
|
||||
If you are using Docker v19.09 or later, you can also add the `--pull always` flag to
|
||||
`docker run` commands, so that Docker automatically checks for available updates
|
||||
(new image layers will only be downloaded if a new version is available).
|
||||
|
||||
## Usage
|
||||
|
||||
We've provided some examples of common CLI commands and how they are best used
|
||||
with this image, since some special considerations must be made.
|
||||
|
||||
- [login](#login) - login to balena
|
||||
- [push](#push) - start a build on the remote balenaCloud build servers, or a local mode device
|
||||
- [logs](#logs) - show device logs
|
||||
- [ssh](#ssh) - SSH into the host or application container of a device
|
||||
- [apps](#app--apps) - list all applications
|
||||
- [app](#app--apps) - display information about a single application
|
||||
- [devices](#device--devices) - list all devices
|
||||
- [device](#device--devices) - show info about a single device
|
||||
- [tunnel](#tunnel) - tunnel local ports to your balenaOS device
|
||||
- [preload](#preload) - preload an app on a disk image (or Edison zip archive)
|
||||
- [build](#build--deploy) - build a project locally
|
||||
- [deploy](#build--deploy) - deploy a single image or a multicontainer project to a balena application
|
||||
- [join](#join--leave) - move a local device to an application on another balena server
|
||||
- [leave](#join--leave) - remove a local device from its balena application
|
||||
- [scan](#scan) - scan for balenaOS devices on your local network
|
||||
|
||||
For each example we have also linked to the corresponding sections of the
|
||||
balena CLI Documentation here: https://www.balena.io/docs/reference/balena-cli
|
||||
|
||||
### login
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#login>
|
||||
|
||||
The `balena login` command can't be used with web authorization and a browser
|
||||
when running in a container. Instead it must be used with `--token` or `--credentials`.
|
||||
|
||||
Notice that here we've used a named volume `balena_data` to store credentials
|
||||
for future runs of the CLI image. This is optional but avoids having to run the login
|
||||
command again every time you run the image.
|
||||
|
||||
```bash
|
||||
$ docker volume create balena_data
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena login --credentials --email "johndoe@gmail.com" --password "secret"
|
||||
> balena login --token "..."
|
||||
> exit
|
||||
```
|
||||
|
||||
### push
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#push-applicationordevice>
|
||||
|
||||
In this example we are mounting your current working directory into the container with `-v "$PWD:$PWD" -w "$PWD"`.
|
||||
This will bind mount your current working directory into the container at the same absolute path.
|
||||
|
||||
This bind mount is required so the CLI has access to your app sources.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "$PWD:$PWD" -w "$PWD" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena push myApp --source .
|
||||
> balena push 10.0.0.1 --env MY_ENV_VAR=value --env my-service:SERVICE_VAR=value
|
||||
> exit
|
||||
```
|
||||
|
||||
### logs
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#logs-device>
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena logs 23c73a1 --service my-service
|
||||
> balena logs 23c73a1.local --system --tail
|
||||
> exit
|
||||
```
|
||||
|
||||
### ssh
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#key-add-name-path>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#ssh-applicationordevice-service>
|
||||
|
||||
The `balena ssh` command requires an existing SSH key added to your balenaCloud
|
||||
account.
|
||||
|
||||
One way to make this key available to the container is to pass the private key file contents as an environment variable.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-e "SSH_PRIVATE_KEY=$(</path/to/priv/key)" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena ssh f49cefd
|
||||
> balena ssh f49cefd my-service
|
||||
> balena ssh 192.168.0.1 --verbose
|
||||
> exit
|
||||
```
|
||||
|
||||
Another way to share SSH keys with the container is to mount your SSH agent socket with keys preloaded.
|
||||
|
||||
```bash
|
||||
$ eval ssh-agent
|
||||
$ ssh-add /path/to/priv/key
|
||||
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "${SSH_AUTH_SOCK}:/ssh-agent" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena ssh f49cefd
|
||||
> balena ssh f49cefd my-service
|
||||
> balena ssh 192.168.0.1 --verbose
|
||||
> exit
|
||||
```
|
||||
|
||||
### app | apps
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#app-nameorslug>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#apps>
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena apps
|
||||
> balena app myorg/myapp
|
||||
> exit
|
||||
```
|
||||
|
||||
### device | devices
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#device-uuid>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#devices>
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena devices --application MyApp
|
||||
> balena device 7cf02a6
|
||||
> exit
|
||||
```
|
||||
|
||||
### tunnel
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#tunnel-deviceorapplication>
|
||||
|
||||
The `balena tunnel` command is easiest used when the host networking stack
|
||||
can be shared with the container and ports can be easily assigned.
|
||||
|
||||
However the host networking driver only works on Linux hosts, and is not supported
|
||||
on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.
|
||||
|
||||
Instead you can bind specific port ranges to the host so you can access the tunnel
|
||||
from outside the container via `localhost:[localPort]`.
|
||||
|
||||
Note that when exposing individual ports, you must specify all interfaces in the format
|
||||
`[remotePort]:0.0.0.0:[localPort]` otherwise the tunnel will only be listening for
|
||||
connections within the container.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-p 22222:22222 \
|
||||
-p 12345:54321
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena tunnel 2ead211 -p 22222:0.0.0.0
|
||||
> balena tunnel myApp -p 54321:0.0.0.0:12345
|
||||
> exit
|
||||
```
|
||||
|
||||
If you have host networking available then you do not need to specify ports
|
||||
in your run command, and the interface `0.0.0.0` is optional in your tunnel command.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
--network host \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena tunnel 2ead211 -p 22222
|
||||
> balena tunnel myApp -p 54321:12345
|
||||
> exit
|
||||
```
|
||||
|
||||
|
||||
|
||||
### preload
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#os-download-type>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#os-configure-image>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#preload-image>
|
||||
|
||||
The `balena preload` command requires access to a Docker client and daemon.
|
||||
|
||||
The easiest way to run this command is to use the included Docker-in-Docker daemon.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "docker_data:/var/lib/docker" \
|
||||
-e "DOCKERD=1" --cap-add SYS_ADMIN \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena os download raspberrypi3 -o raspberry-pi.img
|
||||
> balena os configure raspberry-pi.img --app MyApp
|
||||
> balena preload raspberry-pi.img --app MyApp --commit current
|
||||
> exit
|
||||
```
|
||||
|
||||
Another way to run the `preload` command is to use the host OS Docker socket and avoid
|
||||
starting a Docker daemon in the container. This is achieved with `-v "/var/run/docker.sock:/var/run/docker.sock"`.
|
||||
|
||||
In this example we are mounting your current working directory into the container with `-v "$PWD:$PWD" -w "$PWD"`.
|
||||
This will bind mount your current working directory into the container at the same absolute path.
|
||||
|
||||
This bind mount is required when using the host Docker socket because the absolute path to the balenaOS image
|
||||
file must be the same from both the perspective of the CLI in the container and the host Docker socket.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock" \
|
||||
-v "$PWD:$PWD" -w "$PWD" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena os download raspberrypi3 -o raspberry-pi.img
|
||||
> balena os configure raspberry-pi.img --app MyApp
|
||||
> balena preload raspberry-pi.img --app MyApp --commit current
|
||||
> exit
|
||||
```
|
||||
|
||||
### build | deploy
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#build-source>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#deploy-appname-image>
|
||||
|
||||
The `build` and `deploy` commands both require access to a Docker client and daemon.
|
||||
|
||||
The easiest way to run these commands is to use the included Docker-in-Docker daemon.
|
||||
|
||||
In this example we are mounting your current working directory into the container with `-v "$PWD:$PWD" -w "$PWD"`.
|
||||
This will bind mount your current working directory into the container at the same absolute path.
|
||||
|
||||
This bind mount is required so the CLI has access to your app sources.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "docker_data:/var/lib/docker" \
|
||||
-e DOCKERD=1 --cap-add SYS_ADMIN \
|
||||
-v "$PWD:$PWD" -w "$PWD" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena build --application myApp
|
||||
> balena deploy myApp
|
||||
> exit
|
||||
```
|
||||
|
||||
Another way to run the `build` and `deploy` commands is to use the host OS Docker socket and avoid
|
||||
starting a Docker daemon in the container. This is achieved with `-v "/var/run/docker.sock:/var/run/docker.sock"`.
|
||||
|
||||
In this example we are mounting your current working directory into the container with `-v "$PWD:$PWD" -w "$PWD"`.
|
||||
This will bind mount your current working directory into the container at the same absolute path.
|
||||
|
||||
This bind mount is required so the CLI has access to your app sources.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock" \
|
||||
-v "$PWD:$PWD" -w "$PWD" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena build --application myApp
|
||||
> balena deploy myApp
|
||||
> exit
|
||||
```
|
||||
|
||||
### join | leave
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#join-deviceiporhostname>
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#leave-deviceiporhostname>
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it -v "balena_data:/root/.balena" \
|
||||
balenalib/amd64-debian-balenacli /bin/bash
|
||||
|
||||
> balena join balena.local --application MyApp
|
||||
> balena leave balena.local
|
||||
> exit
|
||||
```
|
||||
|
||||
### scan
|
||||
|
||||
- <https://www.balena.io/docs/reference/balena-cli/#scan>
|
||||
|
||||
The `balena scan` command requires access to the host network interface
|
||||
in order to bind and listen for multicast responses from devices.
|
||||
|
||||
However the host networking driver only works on Linux hosts, and is not supported
|
||||
on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it --network host balenalib/amd64-debian-balenacli scan
|
||||
```
|
||||
|
||||
## Custom images / contributing
|
||||
|
||||
The following steps may be used to create custom CLI images or
|
||||
to contribute bug reports, fixes or features.
|
||||
|
||||
```bash
|
||||
# the currently supported base images are 'debian' and 'alpine'
|
||||
export BALENA_DISTRO="debian"
|
||||
|
||||
# provide the architecture where you will be testing the image
|
||||
export BALENA_ARCH="amd64"
|
||||
|
||||
# optionally register QEMU binfmt if building for other architectures (eg. armv7hf)
|
||||
$ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
# build and tag an image with docker
|
||||
docker build . -f docker/${BALENA_DISTRO}/Dockerfile \
|
||||
--build-arg "BUILD_BASE=balenalib/${BALENA_ARCH}-${BALENA_DISTRO}-node:12.19.1-build" \
|
||||
--build-arg "RUN_BASE=balenalib/${BALENA_ARCH}-${BALENA_DISTRO}-node:12.19.1-run" \
|
||||
--tag "balenalib/${BALENA_ARCH}-${BALENA_DISTRO}-balenacli"
|
||||
```
|
43
docker/alpine/Dockerfile
Normal file
43
docker/alpine/Dockerfile
Normal file
@ -0,0 +1,43 @@
|
||||
ARG BUILD_BASE=balenalib/amd64-alpine-node:12.19.1-build
|
||||
ARG RUN_BASE=balenalib/amd64-alpine-node:12.19.1-run
|
||||
|
||||
FROM ${BUILD_BASE} as build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
# dev dependencies are required for build:fast
|
||||
# --unsafe-perm is not needed because of global /usr/local/etc/npmrc
|
||||
RUN npm install
|
||||
|
||||
RUN npm run build:fast
|
||||
|
||||
# remove dev dependencies after build:fast
|
||||
RUN npm prune --production
|
||||
|
||||
FROM ${RUN_BASE}
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY --from=build /usr/src/app/ .
|
||||
|
||||
ENV PATH $PATH:/usr/src/app/bin
|
||||
|
||||
# fail early if balena binary won't run
|
||||
RUN balena --version
|
||||
|
||||
# https://github.com/balena-io/balena-cli/blob/master/INSTALL-LINUX.md#additional-dependencies
|
||||
RUN install_packages avahi bash ca-certificates docker jq openssh
|
||||
|
||||
COPY docker/docker-init.sh init.sh
|
||||
|
||||
RUN CLI_CMDS=$(jq -r '.commands | keys | map(.[0:index(":")]) | unique | join("\\ ")' < oclif.manifest.json); \
|
||||
sed -ie "s/CLI_CMDS=\"help\"/CLI_CMDS=\"help\\ ${CLI_CMDS}\"/" init.sh && \
|
||||
chmod +x init.sh
|
||||
|
||||
ENTRYPOINT [ "/usr/src/app/init.sh" ]
|
||||
|
||||
CMD [ "help" ]
|
||||
|
||||
ENV SSH_AUTH_SOCK "/ssh-agent"
|
43
docker/debian/Dockerfile
Normal file
43
docker/debian/Dockerfile
Normal file
@ -0,0 +1,43 @@
|
||||
ARG BUILD_BASE=balenalib/amd64-debian-node:12.19.1-build
|
||||
ARG RUN_BASE=balenalib/amd64-debian-node:12.19.1-run
|
||||
|
||||
FROM ${BUILD_BASE} as build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
# dev dependencies are required for build:fast
|
||||
# --unsafe-perm is not needed because of global /usr/local/etc/npmrc
|
||||
RUN npm install
|
||||
|
||||
RUN npm run build:fast
|
||||
|
||||
# remove dev dependencies after build:fast
|
||||
RUN npm prune --production
|
||||
|
||||
FROM ${RUN_BASE}
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY --from=build /usr/src/app/ .
|
||||
|
||||
ENV PATH $PATH:/usr/src/app/bin
|
||||
|
||||
# fail early if balena binary won't run
|
||||
RUN balena --version
|
||||
|
||||
# https://github.com/balena-io/balena-cli/blob/master/INSTALL-LINUX.md#additional-dependencies
|
||||
RUN install_packages avahi-daemon ca-certificates docker.io jq openssh-client
|
||||
|
||||
COPY docker/docker-init.sh init.sh
|
||||
|
||||
RUN CLI_CMDS=$(jq -r '.commands | keys | map(.[0:index(":")]) | unique | join("\\ ")' < oclif.manifest.json); \
|
||||
sed -ie "s/CLI_CMDS=\"help\"/CLI_CMDS=\"help\\ ${CLI_CMDS}\"/" init.sh && \
|
||||
chmod +x init.sh
|
||||
|
||||
ENTRYPOINT [ "/usr/src/app/init.sh" ]
|
||||
|
||||
CMD [ "help" ]
|
||||
|
||||
ENV SSH_AUTH_SOCK "/ssh-agent"
|
30
docker/docker-init.sh
Normal file
30
docker/docker-init.sh
Normal file
@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
# start dockerd if env var is set
|
||||
if [ "${DOCKERD}" = "1" ]
|
||||
then
|
||||
[ -e /var/run/docker.sock ] && rm /var/run/docker.sock
|
||||
dockerd &
|
||||
fi
|
||||
|
||||
# load private ssh key if one is provided
|
||||
if [ -n "${SSH_PRIVATE_KEY}" ]
|
||||
then
|
||||
# if an ssh agent socket was not provided, start our own agent
|
||||
[ -e "${SSH_AUTH_SOCK}" ] || eval "$(ssh-agent -s)"
|
||||
echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add -
|
||||
fi
|
||||
|
||||
# space-separated list of balena CLI commands (filled in through `sed`
|
||||
# in a Dockerfile RUN instruction)
|
||||
CLI_CMDS="help"
|
||||
|
||||
# treat the provided command as a balena CLI arg...
|
||||
# 1. if the first word matches a known entry in CLI_CMDS
|
||||
# 2. OR if the first character is a hyphen (eg. -h or --debug)
|
||||
if [[ " ${CLI_CMDS} " =~ " ${1} " ]] || [ "${1:0:1}" = "-" ]
|
||||
then
|
||||
exec balena "$@"
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
@ -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',
|
||||
|
@ -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');
|
||||
|
@ -20,22 +20,30 @@ import type { ImageDescriptor } from 'resin-compose-parse';
|
||||
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
|
||||
import { dockerignoreHelp, registrySecretsHelp } 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 +53,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
source?: string;
|
||||
build: boolean;
|
||||
nologupload: boolean;
|
||||
'release-tag'?: string[];
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -85,6 +94,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 +125,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
|
||||
@ -151,6 +169,10 @@ ${dockerignoreHelp}
|
||||
'../utils/compose_ts'
|
||||
);
|
||||
|
||||
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||
options['release-tag'] ?? [],
|
||||
);
|
||||
|
||||
if (image) {
|
||||
options['registry-secrets'] = await getRegistrySecrets(
|
||||
sdk,
|
||||
@ -180,7 +202,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 +211,12 @@ ${dockerignoreHelp}
|
||||
buildEmulated: !!options.emulated,
|
||||
buildOpts,
|
||||
});
|
||||
await applyReleaseTagKeysAndValues(
|
||||
sdk,
|
||||
release.id,
|
||||
releaseTagKeys,
|
||||
releaseTagValues,
|
||||
);
|
||||
}
|
||||
|
||||
async deployProject(
|
||||
@ -286,7 +314,7 @@ ${dockerignoreHelp}
|
||||
},
|
||||
);
|
||||
|
||||
let release;
|
||||
let release: Release | ComposeReleaseInfo['release'];
|
||||
if (appType?.is_legacy) {
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
|
||||
@ -344,6 +372,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);
|
||||
}
|
||||
}
|
||||
|
@ -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']),
|
||||
);
|
||||
}
|
||||
}
|
@ -18,20 +18,19 @@
|
||||
import { flags } from '@oclif/command';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
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;
|
||||
@ -88,15 +87,19 @@ export default class OsConfigureCmd extends Command {
|
||||
|
||||
${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 +121,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 +155,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 +165,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 +174,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 +266,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');
|
||||
|
@ -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;
|
||||
}
|
||||
@ -62,13 +65,16 @@ export default class PreloadCmd extends Command {
|
||||
When the device boots, it will not need to download the application, as it was
|
||||
preloaded.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
Warning: "balena preload" requires Docker to be correctly installed in
|
||||
your shell environment. For more information (including Windows support)
|
||||
check: https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png',
|
||||
'$ balena preload balena.img --app MyApp --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0',
|
||||
'$ balena preload balena.img --app myorg/myapp --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png',
|
||||
'$ balena preload balena.img',
|
||||
];
|
||||
|
||||
@ -83,10 +89,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 +104,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 +166,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 +215,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 +247,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);
|
||||
|
@ -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) {
|
||||
|
24
lib/utils/common-args.ts
Normal file
24
lib/utils/common-args.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @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 { lowercaseIfSlug } from './normalization';
|
||||
|
||||
export const applicationRequired = {
|
||||
name: 'application',
|
||||
description: 'application name, slug (preferred), or numeric ID (deprecated)',
|
||||
required: true,
|
||||
parse: lowercaseIfSlug,
|
||||
};
|
@ -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,9 @@ export const drive = flags.string({
|
||||
Check \`balena util available-drives\` for available options.
|
||||
`,
|
||||
});
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
|
@ -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,52 @@ 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.`,
|
||||
);
|
||||
try {
|
||||
const yaml = await import('js-yaml');
|
||||
const compose = yaml.load(composeStr);
|
||||
const devOverlay = yaml.load(await fs.readFile(devOverlayPath, 'utf8'));
|
||||
// We only want to merge the services section
|
||||
compose.services = { ...compose.services, ...devOverlay.services };
|
||||
composeStr = yaml.dump(compose);
|
||||
} 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 +223,7 @@ async function resolveProject(
|
||||
if (!quiet && !composeFileName) {
|
||||
logger.logInfo(`No "docker-compose.yml" file found at "${projectRoot}"`);
|
||||
}
|
||||
|
||||
return [composeFileName, composeFileContents];
|
||||
}
|
||||
|
||||
@ -1128,15 +1225,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 +1296,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-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 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,81 @@ 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');
|
||||
let gotSignal = false;
|
||||
const handleSignal = () => {
|
||||
gotSignal = true;
|
||||
logs.emit('close');
|
||||
};
|
||||
addSIGINTHandler(handleSignal);
|
||||
process.once('SIGTERM', handleSignal);
|
||||
try {
|
||||
await new Promise((_resolve, reject) => {
|
||||
logs.on('data', (log) => {
|
||||
displayLogLine(log, logger, system, filterServices);
|
||||
});
|
||||
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());
|
||||
}
|
||||
});
|
||||
});
|
||||
} 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 {
|
||||
|
@ -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,
|
||||
);
|
||||
|
||||
|
@ -22,7 +22,7 @@ import * as os from 'os';
|
||||
import type * as ShellEscape from 'shell-escape';
|
||||
|
||||
import type { Device, PineOptions } from 'balena-sdk';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { ExpectedError, SIGINTError } from '../errors';
|
||||
import { getBalenaSdk, getChalk, getVisuals } from './lazy';
|
||||
import { promisify } from 'util';
|
||||
import { isSubcommand } from '../preparser';
|
||||
@ -204,39 +204,66 @@ 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> {
|
||||
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();
|
||||
@ -490,3 +517,61 @@ export const expandForAppName: PineOptions<Device> = {
|
||||
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 = () => {
|
||||
reject(new SIGINTError('Task aborted on SIGINT signal'));
|
||||
};
|
||||
addSIGINTHandler(sigintHandler);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([sigintPromise, task(...theArgs)]);
|
||||
} finally {
|
||||
process.removeListener('SIGINT', sigintHandler);
|
||||
}
|
||||
}
|
||||
|
@ -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,24 @@ 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/).`;
|
||||
|
@ -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;
|
||||
}
|
||||
|
211
npm-shrinkwrap.json
generated
211
npm-shrinkwrap.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "12.29.0",
|
||||
"version": "12.42.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -1673,9 +1673,9 @@
|
||||
"integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w=="
|
||||
},
|
||||
"@types/memoizee": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.4.tgz",
|
||||
"integrity": "sha512-c9+1g6+6vEqcw5UuM0RbfQV0mssmZcoG9+hNC5ptDCsv4G+XJW1Z4pE13wV5zbc9e0+YrDydALBTiD3nWG1a3g=="
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.5.tgz",
|
||||
"integrity": "sha512-+ZzZZ3+0a7/ajBPeAAD4+LxrBsCat0EFZQtO3o0rwpIeLmDmSaM8KF/oYPuFxeUFAMiHIHFcGucFnY/8S4Hszg=="
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "2.0.3",
|
||||
@ -1995,9 +1995,9 @@
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"abortcontroller-polyfill": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.5.0.tgz",
|
||||
"integrity": "sha512-O6Xk757Jb4o0LMzMOMdWvxpHWrQzruYBaUruFaIOfAQRnWFxfdXYobw12jrVHGtoXk6WiiyYzc0QWN9aL62HQA=="
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.1.tgz",
|
||||
"integrity": "sha512-yml9NiDEH4M4p0G4AcPkg8AAa4mF3nfYF28VQxaokpO67j9H7gWgmsVWJ/f1Rn+PzsnDYvzJzWIQzCqDKRvWlA=="
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
@ -2525,13 +2525,13 @@
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"balena-auth": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balena-auth/-/balena-auth-4.0.2.tgz",
|
||||
"integrity": "sha512-a0IfAN53aQpFOKtgKK+MSLMVZC/HsHZLiDsJhpPKTUd257fEcnmQWzBYxot9ny9NfJhhhoyalcu5e4RSH0TsiQ==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-auth/-/balena-auth-4.1.0.tgz",
|
||||
"integrity": "sha512-weKEmWnlb2cYNLr8pqaVdCP+uTfxBLQU2oHzn6M9mlF6+mqIaQTmkj+/8fMilEnm32qhYYdOyz4y3H+7kLIcIw==",
|
||||
"requires": {
|
||||
"@types/jwt-decode": "^2.2.1",
|
||||
"balena-errors": "^4.2.1",
|
||||
"balena-settings-storage": "^6.0.0",
|
||||
"balena-errors": "^4.7.1",
|
||||
"balena-settings-storage": "^7.0.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
@ -2562,9 +2562,9 @@
|
||||
}
|
||||
},
|
||||
"balena-errors": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/balena-errors/-/balena-errors-4.4.1.tgz",
|
||||
"integrity": "sha512-912lPp1LyBjkpxRg6m/EpOCssqMhgkzyYbrKwtT2uRvixm89WOlJrj5sPkxnbPnp5IoMNaoRONxFt1jtiQf50Q==",
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/balena-errors/-/balena-errors-4.7.1.tgz",
|
||||
"integrity": "sha512-g21kf6N5tMZYDietZNLHCbqhmAxPX9gRmJQgMuIjMZWvjzCQxcqaELNYTtDwXwEbXLhbhF6QV2IJDZul+5X6nQ==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0",
|
||||
"typed-error": "^3.0.0"
|
||||
@ -2610,20 +2610,30 @@
|
||||
}
|
||||
},
|
||||
"balena-pine": {
|
||||
"version": "12.3.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-pine/-/balena-pine-12.3.0.tgz",
|
||||
"integrity": "sha512-HKC/7Aqfd4YEqx8y2/KfhzrYHW68i3rhbGeTMRCFluLAzsg9YvzBjvLFHLaIyv4IBLqwgsEkjxCpz1Qzyq3XUw==",
|
||||
"version": "12.4.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-pine/-/balena-pine-12.4.0.tgz",
|
||||
"integrity": "sha512-ap4WvIwwEsLl5rh7badxiu/4pDqbgT3DdeSkKl03T9U4XQ8fgFuqcl//lvpUG1jzhSe/ExQfkv1ZebrUd/kvqA==",
|
||||
"requires": {
|
||||
"@balena/es-version": "^1.0.0",
|
||||
"balena-errors": "^4.2.1",
|
||||
"pinejs-client-core": "^6.6.1",
|
||||
"pinejs-client-core": "^6.9.0",
|
||||
"tslib": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinejs-client-core": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.9.4.tgz",
|
||||
"integrity": "sha512-saScuq6J3NIjOvTeHUVZSK/pxF+uwgxxbBjffN2WUUpkz846SGGFzKv89Y73FRuP5bT25gFHJ4W4ZabXOmqI5A==",
|
||||
"requires": {
|
||||
"@balena/es-version": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"balena-preload": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-10.3.1.tgz",
|
||||
"integrity": "sha512-pz0IRzi2ByjgGROO9ryMlRI24RIp1IMfx7zcLyf99cOveOPFwCgu0N2CE8kkq9hnmlJuqqDoOYLqkoTJ5zv8xw==",
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-10.4.1.tgz",
|
||||
"integrity": "sha512-/DHvtF7qPg3cfHfZxP3+EInqtqlwD/czTyIxBMnieZb/4UMISL/6fXPFsVYhxTwAeNmTsBaH+KTj4Owb5Lz5AA==",
|
||||
"requires": {
|
||||
"archiver": "^3.1.1",
|
||||
"balena-sdk": "^15.3.1",
|
||||
@ -2635,6 +2645,7 @@
|
||||
"get-port": "^3.2.0",
|
||||
"lodash": "^4.17.20",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"request-promise": "^4.2.6",
|
||||
"resin-cli-visuals": "^1.7.0",
|
||||
"tar-fs": "^2.1.0",
|
||||
"tmp": "0.0.33",
|
||||
@ -2698,13 +2709,13 @@
|
||||
}
|
||||
},
|
||||
"balena-request": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.1.1.tgz",
|
||||
"integrity": "sha512-PsIbPtEOo84E8AxlUbyuEnnX3yd7A0SGFW1T/L7QcVlxQPPMgWW0SdPU94bZAT7futBxZ+ha7yFKHJ3VlO7uIg==",
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.4.0.tgz",
|
||||
"integrity": "sha512-wfPaWX/+NgT2xNplQqA8oCNLJXG6eLMbf9IOX8T4ZX+nqBoA9bydoIRLunGExMNfUWpxApvBh5ls8fJOd9VTjQ==",
|
||||
"requires": {
|
||||
"@balena/node-web-streams": "^0.2.3",
|
||||
"balena-errors": "^4.4.0",
|
||||
"fetch-ponyfill": "^6.1.1",
|
||||
"balena-errors": "^4.7.1",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"fetch-readablestream": "^0.2.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"qs": "^6.9.4",
|
||||
@ -2712,21 +2723,21 @@
|
||||
}
|
||||
},
|
||||
"balena-sdk": {
|
||||
"version": "15.6.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.6.0.tgz",
|
||||
"integrity": "sha512-yurPtF7+loQVcoPfWjJQV9WziMaLUxvppGZPOVH5GVOoWYjQSaO6avtOw97VLmMglufQIV+PN/BoseIRG7XxXg==",
|
||||
"version": "15.29.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.29.0.tgz",
|
||||
"integrity": "sha512-7m5Auj5Xus5dvXC7yzJgT1a9P/fIDlK/R7c6l6X3ISL8nuhR5ZKu4SmnJnmEIOvBMHsCjDqVn76CxYu3tDIm9g==",
|
||||
"requires": {
|
||||
"@balena/es-version": "^1.0.0",
|
||||
"@types/lodash": "^4.14.159",
|
||||
"@types/memoizee": "^0.4.3",
|
||||
"@types/node": "^10.17.28",
|
||||
"abortcontroller-polyfill": "^1.5.0",
|
||||
"balena-auth": "^4.0.2",
|
||||
"balena-errors": "^4.4.0",
|
||||
"balena-auth": "^4.1.0",
|
||||
"balena-errors": "^4.7.1",
|
||||
"balena-hup-action-utils": "~4.0.2",
|
||||
"balena-pine": "^12.3.0",
|
||||
"balena-pine": "^12.4.0",
|
||||
"balena-register-device": "^7.1.0",
|
||||
"balena-request": "^11.1.1",
|
||||
"balena-request": "^11.2.0",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^4.0.4",
|
||||
"lodash": "^4.17.19",
|
||||
@ -2749,9 +2760,9 @@
|
||||
}
|
||||
},
|
||||
"balena-settings-client": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.5.tgz",
|
||||
"integrity": "sha512-w1SWIQYViMP51PYnPvbwgGavipkBv8wbRj1ISjPYZ5M45oEVRcktDfix8c3xOlWl+vWqW8aA4L8BjhqnxhAvRQ==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.6.tgz",
|
||||
"integrity": "sha512-bB14Zvg1N6t7XXPJqZs48SajgTuk2WTMm2AnxcOfoIQ2d/Lh0RsEGxD9toF2v+WhF2Ip4u7ko5tKlCr2kFddXA==",
|
||||
"requires": {
|
||||
"@resin.io/types-hidepath": "1.0.1",
|
||||
"@resin.io/types-home-or-tmp": "3.0.0",
|
||||
@ -2771,11 +2782,12 @@
|
||||
}
|
||||
},
|
||||
"balena-settings-storage": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/balena-settings-storage/-/balena-settings-storage-6.0.1.tgz",
|
||||
"integrity": "sha512-jdDoKzbJXlF696EZSbwD6lZ1dMe98aUtx7btFE4j4PRCSeh2BWx5P5VLGh9Bk3sH2FUcqYg0iw/wdKvkcv44oA==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-settings-storage/-/balena-settings-storage-7.0.0.tgz",
|
||||
"integrity": "sha512-gufzVJznyt9e1CvpBuLe2caU5KcEwl1YHCbK5OMz09zXDA2OMAICPXsLlViK+KiuZwZrBx3tyU2FZjAzRZFgwQ==",
|
||||
"requires": {
|
||||
"@types/node": "^10.17.26",
|
||||
"balena-errors": "^4.7.1",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@ -3202,9 +3214,9 @@
|
||||
"integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g=="
|
||||
},
|
||||
"buffer-indexof-polyfill": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz",
|
||||
"integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8="
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
|
||||
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="
|
||||
},
|
||||
"buffer-shims": {
|
||||
"version": "1.0.0",
|
||||
@ -3278,6 +3290,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"call-me-maybe": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
|
||||
@ -6175,11 +6196,18 @@
|
||||
}
|
||||
},
|
||||
"fetch-ponyfill": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-6.1.1.tgz",
|
||||
"integrity": "sha512-rWLgTr5A44/XhvCQPYj0X9Tc+cjUaHofSM4lcwjc9MavD5lkjIhJ+h8JQlavPlTIgDpwhuRozaIykBvX9ItaSA==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz",
|
||||
"integrity": "sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==",
|
||||
"requires": {
|
||||
"node-fetch": "~2.6.0"
|
||||
"node-fetch": "~2.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"fetch-readablestream": {
|
||||
@ -6916,6 +6944,16 @@
|
||||
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"get-port": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
|
||||
@ -10057,18 +10095,18 @@
|
||||
}
|
||||
},
|
||||
"memoizee": {
|
||||
"version": "0.4.14",
|
||||
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz",
|
||||
"integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==",
|
||||
"version": "0.4.15",
|
||||
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz",
|
||||
"integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==",
|
||||
"requires": {
|
||||
"d": "1",
|
||||
"es5-ext": "^0.10.45",
|
||||
"es6-weak-map": "^2.0.2",
|
||||
"d": "^1.0.1",
|
||||
"es5-ext": "^0.10.53",
|
||||
"es6-weak-map": "^2.0.3",
|
||||
"event-emitter": "^0.3.5",
|
||||
"is-promise": "^2.1",
|
||||
"lru-queue": "0.1",
|
||||
"next-tick": "1",
|
||||
"timers-ext": "^0.1.5"
|
||||
"is-promise": "^2.2.2",
|
||||
"lru-queue": "^0.1.0",
|
||||
"next-tick": "^1.1.0",
|
||||
"timers-ext": "^0.1.7"
|
||||
}
|
||||
},
|
||||
"meow": {
|
||||
@ -10999,7 +11037,8 @@
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==",
|
||||
"dev": true
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "4.2.3",
|
||||
@ -12780,9 +12819,12 @@
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.0.tgz",
|
||||
"integrity": "sha512-yjACOWijC6L/kmPZZAsVBNY2zfHSIbpdpL977quseu56/8BZ2LoF5axK2bGhbzhVKt7V9xgWTtpyLbxwIoER0Q==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
@ -13321,6 +13363,25 @@
|
||||
"throttleit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"request-promise": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz",
|
||||
"integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==",
|
||||
"requires": {
|
||||
"bluebird": "^3.5.0",
|
||||
"request-promise-core": "1.1.4",
|
||||
"stealthy-require": "^1.1.1",
|
||||
"tough-cookie": "^2.3.3"
|
||||
}
|
||||
},
|
||||
"request-promise-core": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz",
|
||||
"integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@ -14023,6 +14084,23 @@
|
||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"object-inspect": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
|
||||
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
@ -14517,6 +14595,11 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||
},
|
||||
"stealthy-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
|
||||
},
|
||||
"stream-chunker": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/stream-chunker/-/stream-chunker-1.2.8.tgz",
|
||||
@ -15807,9 +15890,9 @@
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
|
||||
"integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.2.0",
|
||||
|
17
package.json
17
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "12.29.0",
|
||||
"version": "12.42.0",
|
||||
"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"
|
||||
@ -197,14 +198,14 @@
|
||||
"JSONStream": "^1.0.3",
|
||||
"balena-config-json": "^4.1.0",
|
||||
"balena-device-init": "^5.0.2",
|
||||
"balena-errors": "^4.4.1",
|
||||
"balena-errors": "^4.7.1",
|
||||
"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.29.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",
|
||||
|
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) {
|
@ -185,6 +185,7 @@ export class BalenaAPIMock extends NockMock {
|
||||
public expectGetDevice(opts: {
|
||||
fullUUID: string;
|
||||
inaccessibleApp?: boolean;
|
||||
isOnline?: boolean;
|
||||
optional?: boolean;
|
||||
persist?: boolean;
|
||||
}) {
|
||||
@ -194,6 +195,7 @@ export class BalenaAPIMock extends NockMock {
|
||||
{
|
||||
id,
|
||||
uuid: opts.fullUUID,
|
||||
is_online: opts.isOnline,
|
||||
belongs_to__application: opts.inaccessibleApp
|
||||
? []
|
||||
: [{ app_name: 'test' }],
|
||||
|
@ -257,12 +257,16 @@ describe('balena build', function () {
|
||||
const qemuMod = require(qemuModPath);
|
||||
const qemuBinPath = await qemuMod.getQemuPath(arch);
|
||||
try {
|
||||
// patch fs.access and fs.stat to pretend that a copy of the Qemu binary
|
||||
// already exists locally, thus preventing a download during tests
|
||||
mock(fsModPath, {
|
||||
...fsMod,
|
||||
promises: {
|
||||
...fsMod.promises,
|
||||
access: async (p: string) =>
|
||||
p === qemuBinPath ? undefined : fsMod.promises.access(p),
|
||||
stat: async (p: string) =>
|
||||
p === qemuBinPath ? { size: 1 } : fsMod.promises.stat(p),
|
||||
},
|
||||
});
|
||||
mock(qemuModPath, {
|
||||
|
@ -19,32 +19,6 @@ import { expect } from 'chai';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
const HELP_RESPONSE = `
|
||||
Move one or more devices to another application.
|
||||
|
||||
USAGE
|
||||
$ balena device move <uuid(s)>
|
||||
|
||||
ARGUMENTS
|
||||
<uuid> comma-separated list (no blank spaces) of device UUIDs to be moved
|
||||
|
||||
OPTIONS
|
||||
-a, --application <application> application name
|
||||
-h, --help show CLI help
|
||||
--app <app> same as '--application'
|
||||
|
||||
DESCRIPTION
|
||||
Move one or more devices to another application.
|
||||
|
||||
Note, if the application option is omitted it will be prompted
|
||||
for interactively.
|
||||
|
||||
EXAMPLES
|
||||
$ balena device move 7cf02a6
|
||||
$ balena device move 7cf02a6,dc39e52
|
||||
$ balena device move 7cf02a6 --application MyNewApp
|
||||
`;
|
||||
|
||||
describe('balena device move', function () {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
@ -59,14 +33,6 @@ describe('balena device move', function () {
|
||||
api.done();
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
const { out, err } = await runCommand('device move -h');
|
||||
|
||||
expect(cleanOutput(out)).to.deep.equal(cleanOutput([HELP_RESPONSE]));
|
||||
|
||||
expect(err).to.eql([]);
|
||||
});
|
||||
|
||||
it('should error if uuid not provided', async () => {
|
||||
const { out, err } = await runCommand('device move');
|
||||
const errLines = cleanOutput(err);
|
||||
|
@ -21,36 +21,6 @@ import * as path from 'path';
|
||||
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
const HELP_RESPONSE = `
|
||||
List all devices.
|
||||
|
||||
USAGE
|
||||
$ balena devices
|
||||
|
||||
OPTIONS
|
||||
-a, --application <application> application name
|
||||
-h, --help show CLI help
|
||||
-j, --json produce JSON output instead of tabular output
|
||||
--app <app> same as '--application'
|
||||
|
||||
DESCRIPTION
|
||||
list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the \`--application\` option.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because field names are less likely to change in JSON format and because it
|
||||
better represents data types like arrays, empty strings and null values.
|
||||
The 'jq' utility may be helpful for querying JSON fields in shell scripts
|
||||
(https://stedolan.github.io/jq/manual/).
|
||||
|
||||
EXAMPLES
|
||||
$ balena devices
|
||||
$ balena devices --application MyApp
|
||||
$ balena devices --app MyApp
|
||||
$ balena devices -a MyApp
|
||||
`;
|
||||
|
||||
describe('balena devices', function () {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
@ -65,14 +35,6 @@ describe('balena devices', function () {
|
||||
api.done();
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
const { out, err } = await runCommand('devices -h');
|
||||
|
||||
expect(cleanOutput(out)).to.deep.equal(cleanOutput([HELP_RESPONSE]));
|
||||
|
||||
expect(err).to.eql([]);
|
||||
});
|
||||
|
||||
it('should list devices from own and collaborator apps', async () => {
|
||||
api.scope
|
||||
.get(
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { isV12 } from '../../../build/utils/version';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
@ -50,9 +49,7 @@ describe('balena devices supported', function () {
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
|
||||
expect(lines[0].replace(/ +/g, ' ')).to.equal(
|
||||
isV12() ? 'SLUG ALIASES ARCH NAME' : 'SLUG NAME',
|
||||
);
|
||||
expect(lines[0].replace(/ +/g, ' ')).to.equal('SLUG ALIASES ARCH NAME');
|
||||
expect(lines).to.have.lengthOf.at.least(2);
|
||||
|
||||
// Discontinued devices should be filtered out from results
|
||||
|
489
tests/commands/env/envs.spec.ts
vendored
489
tests/commands/env/envs.spec.ts
vendored
@ -18,7 +18,6 @@
|
||||
import { expect } from 'chai';
|
||||
import { stripIndent } from '../../../lib/utils/lazy';
|
||||
|
||||
import { isV12 } from '../../../lib/utils/version';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { runCommand } from '../../helpers';
|
||||
|
||||
@ -43,38 +42,22 @@ describe('balena envs', function () {
|
||||
});
|
||||
|
||||
it('should successfully list env vars for a test app', async () => {
|
||||
if (isV12()) {
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -a ${appName}`);
|
||||
const { out, err } = await runCommand(`envs -a ${appName}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION SERVICE
|
||||
120110 svar1 svar1-value test service1
|
||||
120111 svar2 svar2-value test service2
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
} else {
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -a ${appName}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE
|
||||
120101 var1 var1-val
|
||||
120102 var2 22
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
}
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION SERVICE
|
||||
120110 svar1 svar1-value test service1
|
||||
120111 svar2 svar2-value test service2
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
it('should successfully list config vars for a test app', async () => {
|
||||
@ -83,21 +66,12 @@ describe('balena envs', function () {
|
||||
|
||||
const { out, err } = await runCommand(`envs -a ${appName} --config`);
|
||||
|
||||
if (isV12()) {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test
|
||||
` + '\n',
|
||||
);
|
||||
} else {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false
|
||||
` + '\n',
|
||||
);
|
||||
}
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test
|
||||
` + '\n',
|
||||
);
|
||||
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
@ -108,24 +82,14 @@ describe('balena envs', function () {
|
||||
|
||||
const { out, err } = await runCommand(`envs -cja ${appName}`);
|
||||
|
||||
if (isV12()) {
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal([
|
||||
{
|
||||
appName: 'test',
|
||||
id: 120300,
|
||||
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
|
||||
value: 'false',
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal([
|
||||
{
|
||||
id: 120300,
|
||||
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
|
||||
value: 'false',
|
||||
},
|
||||
]);
|
||||
}
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal([
|
||||
{
|
||||
appName: 'test',
|
||||
id: 120300,
|
||||
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
|
||||
value: 'false',
|
||||
},
|
||||
]);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
@ -133,138 +97,70 @@ describe('balena envs', function () {
|
||||
const serviceName = 'service2';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
if (isV12()) {
|
||||
api.expectGetAppEnvVars();
|
||||
}
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -a ${appName} -s ${serviceName}`,
|
||||
);
|
||||
|
||||
if (isV12()) {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION SERVICE
|
||||
120111 svar2 svar2-value test service2
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
` + '\n',
|
||||
);
|
||||
} else {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE
|
||||
120111 svar2 svar2-value
|
||||
` + '\n',
|
||||
);
|
||||
}
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION SERVICE
|
||||
120111 svar2 svar2-value test service2
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
if (!isV12()) {
|
||||
it('should produce an empty JSON array when no app service variables exist', async () => {
|
||||
const serviceName = 'nono';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppServiceVars();
|
||||
it('should successfully list env and service vars for a test app (-s flags)', async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -a ${appName} -s ${serviceName} -j`,
|
||||
);
|
||||
const { out, err } = await runCommand(
|
||||
`envs -a ${appName} -s ${serviceName}`,
|
||||
);
|
||||
|
||||
expect(out.join('')).to.equal('[]\n');
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
}
|
||||
|
||||
if (!isV12()) {
|
||||
it('should successfully list env and service vars for a test app (--all flag)', async () => {
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -a ${appName} --all`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION SERVICE
|
||||
120110 svar1 svar1-value test service1
|
||||
120111 svar2 svar2-value test service2
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
}
|
||||
|
||||
it(
|
||||
isV12()
|
||||
? 'should successfully list env and service vars for a test app (-s flags)'
|
||||
: 'should successfully list env and service vars for a test app (--all -s flags)',
|
||||
async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
isV12()
|
||||
? `envs -a ${appName} -s ${serviceName}`
|
||||
: `envs -a ${appName} --all -s ${serviceName}`,
|
||||
);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION SERVICE
|
||||
120110 svar1 svar1-value test ${serviceName}
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
},
|
||||
);
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
it('should successfully list env variables for a test device', async () => {
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
if (isV12()) {
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
}
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(`envs -d ${uuid}`);
|
||||
|
||||
if (isV12()) {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120110 svar1 svar1-value test * service1
|
||||
120111 svar2 svar2-value test * service2
|
||||
120120 svar3 svar3-value test ${uuid} service1
|
||||
120121 svar4 svar4-value test ${uuid} service2
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
} else {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE
|
||||
120203 var3 var3-val
|
||||
120204 var4 44
|
||||
` + '\n',
|
||||
);
|
||||
}
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120110 svar1 svar1-value test * service1
|
||||
120111 svar2 svar2-value test * service2
|
||||
120120 svar3 svar3-value test ${uuid} service1
|
||||
120121 svar4 svar4-value test ${uuid} service2
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
@ -272,42 +168,25 @@ describe('balena envs', function () {
|
||||
it('should successfully list env variables for a test device (JSON output)', async () => {
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
if (isV12()) {
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
}
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
|
||||
|
||||
if (isV12()) {
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(
|
||||
JSON.parse(`[
|
||||
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
|
||||
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
|
||||
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" },
|
||||
{ "id": 120111, "appName": "test", "deviceUUID": "*", "name": "svar2", "value": "svar2-value", "serviceName": "service2" },
|
||||
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "service1" },
|
||||
{ "id": 120121, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar4", "value": "svar4-value", "serviceName": "service2" },
|
||||
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
|
||||
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
|
||||
]`),
|
||||
);
|
||||
} else {
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal([
|
||||
{
|
||||
id: 120203,
|
||||
name: 'var3',
|
||||
value: 'var3-val',
|
||||
},
|
||||
{
|
||||
id: 120204,
|
||||
name: 'var4',
|
||||
value: '44',
|
||||
},
|
||||
]);
|
||||
}
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(
|
||||
JSON.parse(`[
|
||||
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
|
||||
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
|
||||
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" },
|
||||
{ "id": 120111, "appName": "test", "deviceUUID": "*", "name": "svar2", "value": "svar2-value", "serviceName": "service2" },
|
||||
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "service1" },
|
||||
{ "id": 120121, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar4", "value": "svar4-value", "serviceName": "service2" },
|
||||
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
|
||||
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
|
||||
]`),
|
||||
);
|
||||
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
@ -315,29 +194,18 @@ describe('balena envs', function () {
|
||||
it('should successfully list config variables for a test device', async () => {
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceConfigVars();
|
||||
if (isV12()) {
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppConfigVars();
|
||||
}
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppConfigVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -d ${shortUUID} --config`);
|
||||
|
||||
if (isV12()) {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test *
|
||||
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 test ${shortUUID}
|
||||
` + '\n',
|
||||
);
|
||||
} else {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE
|
||||
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900
|
||||
` + '\n',
|
||||
);
|
||||
}
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test *
|
||||
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 test ${shortUUID}
|
||||
` + '\n',
|
||||
);
|
||||
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
@ -348,85 +216,28 @@ describe('balena envs', function () {
|
||||
api.expectGetApplication();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceServiceVars();
|
||||
if (isV12()) {
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDeviceEnvVars();
|
||||
}
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDeviceEnvVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
|
||||
|
||||
if (isV12()) {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120111 svar2 svar2-value test * service2
|
||||
120121 svar4 svar4-value test ${uuid} service2
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
} else {
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE
|
||||
120121 svar4 svar4-value
|
||||
` + '\n',
|
||||
);
|
||||
}
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120111 svar2 svar2-value test * service2
|
||||
120121 svar4 svar4-value test ${uuid} service2
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
if (!isV12()) {
|
||||
it('should produce an empty JSON array when no device service variables exist', async () => {
|
||||
const serviceName = 'nono';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -d ${shortUUID} -s ${serviceName} -j`,
|
||||
);
|
||||
|
||||
expect(out.join('')).to.equal('[]\n');
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
}
|
||||
|
||||
if (!isV12()) {
|
||||
it('should successfully list env and service variables for a test device (--all flag)', async () => {
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(`envs -d ${uuid} --all`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120110 svar1 svar1-value test * service1
|
||||
120111 svar2 svar2-value test * service2
|
||||
120120 svar3 svar3-value test ${uuid} service1
|
||||
120121 svar4 svar4-value test ${uuid} service2
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
}
|
||||
|
||||
it('should successfully list env and service variables for a test device (unknown app)', async () => {
|
||||
api.expectGetDevice({ fullUUID, inaccessibleApp: true });
|
||||
api.expectGetDeviceEnvVars();
|
||||
@ -434,9 +245,7 @@ describe('balena envs', function () {
|
||||
|
||||
const uuid = shortUUID;
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
isV12() ? `envs -d ${uuid}` : `envs -d ${uuid} --all`,
|
||||
);
|
||||
const { out, err } = await runCommand(`envs -d ${uuid}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
@ -450,29 +259,21 @@ describe('balena envs', function () {
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
it(
|
||||
isV12()
|
||||
? 'should successfully list env and service vars for a test device (-s flags)'
|
||||
: 'should successfully list env and service vars for a test device (--all -s flags)',
|
||||
async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
it('should successfully list env and service vars for a test device (-s flags)', async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(
|
||||
isV12()
|
||||
? `envs -d ${uuid} -s ${serviceName}`
|
||||
: `envs -d ${uuid} --all -s ${serviceName}`,
|
||||
);
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120110 svar1 svar1-value test * ${serviceName}
|
||||
120120 svar3 svar3-value test ${uuid} ${serviceName}
|
||||
@ -481,33 +282,26 @@ describe('balena envs', function () {
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
},
|
||||
);
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
it(
|
||||
isV12()
|
||||
? 'should successfully list env and service vars for a test device (-js flags)'
|
||||
: 'should successfully list env and service vars for a test device (--all -js flags)',
|
||||
async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
it('should successfully list env and service vars for a test device (-js flags)', async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
isV12()
|
||||
? `envs -d ${shortUUID} -js ${serviceName}`
|
||||
: `envs -d ${shortUUID} --all -js ${serviceName}`,
|
||||
);
|
||||
const { out, err } = await runCommand(
|
||||
`envs -d ${shortUUID} -js ${serviceName}`,
|
||||
);
|
||||
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(
|
||||
JSON.parse(`[
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(
|
||||
JSON.parse(`[
|
||||
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
|
||||
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
|
||||
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
|
||||
@ -515,8 +309,7 @@ describe('balena envs', function () {
|
||||
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
|
||||
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
|
||||
]`),
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
},
|
||||
);
|
||||
);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
});
|
||||
|
@ -46,20 +46,33 @@ describe('balena logs', function () {
|
||||
itS('should reach the expected endpoints on a local device', async () => {
|
||||
supervisor.expectGetPing();
|
||||
supervisor.expectGetLogs();
|
||||
supervisor.expectGetLogs();
|
||||
|
||||
const { err, out } = await runCommand('logs 1.2.3.4');
|
||||
const { err, out } = await runCommand('logs 1.2.3.4 --max-retry 1');
|
||||
|
||||
expect(err).to.be.empty;
|
||||
const errLines = cleanOutput(err, true);
|
||||
const errMsg =
|
||||
'Max retry count (1) exceeded while attempting to reconnect to the device';
|
||||
if (process.env.DEBUG) {
|
||||
expect(errLines).to.include(errMsg);
|
||||
} else {
|
||||
expect(errLines).to.have.members([errMsg]);
|
||||
}
|
||||
|
||||
const removeTimestamps = (logLine: string) =>
|
||||
logLine.replace(/(?<=\[Logs\]) \[.+?\]/, '');
|
||||
const cleanedOut = cleanOutput(out, true).map((l) => removeTimestamps(l));
|
||||
|
||||
expect(cleanedOut).to.deep.equal([
|
||||
expect(cleanedOut).to.have.members([
|
||||
'[Logs] Streaming logs',
|
||||
'[Logs] [bar] bar 8 (332) Linux 4e3f81149d71 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux',
|
||||
'[Logs] [foo] foo 8 (200) Linux cc5df60d89ee 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux',
|
||||
'[Error] Connection to device lost',
|
||||
'[Warn] Connection to device lost',
|
||||
'Retrying "Streaming logs" after 1.0s (1 of 1) due to: DeviceConnectionLostError: Connection to device lost',
|
||||
'[Logs] Streaming logs',
|
||||
'[Logs] [bar] bar 8 (332) Linux 4e3f81149d71 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux',
|
||||
'[Logs] [foo] foo 8 (200) Linux cc5df60d89ee 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux',
|
||||
'[Warn] Connection to device lost',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -66,8 +66,7 @@ const commonResponseLines = {
|
||||
};
|
||||
|
||||
const commonQueryParams = [
|
||||
['owner', 'bob'],
|
||||
['app', 'testApp'],
|
||||
['slug', 'gh_user/testApp'],
|
||||
['dockerfilePath', ''],
|
||||
['emulated', 'false'],
|
||||
['nocache', 'false'],
|
||||
@ -87,7 +86,7 @@ describe('balena push', function () {
|
||||
builder = new BuilderMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
api.expectGetMyApplication();
|
||||
api.expectGetApplication();
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
@ -145,7 +144,7 @@ describe('balena push', function () {
|
||||
|
||||
await testPushBuildStream({
|
||||
builderMock: builder,
|
||||
commandLine: `push testApp --source ${projectPath} -R ${regSecretsPath} -G`,
|
||||
commandLine: `push testApp --source ${projectPath} -R ${regSecretsPath}`,
|
||||
expectedFiles,
|
||||
expectedQueryParams: commonQueryParams,
|
||||
expectedResponseLines,
|
||||
@ -345,7 +344,7 @@ describe('balena push', function () {
|
||||
|
||||
await testPushBuildStream({
|
||||
builderMock: builder,
|
||||
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -G`,
|
||||
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath}`,
|
||||
expectedFiles,
|
||||
expectedQueryParams: commonQueryParams,
|
||||
expectedResponseLines: commonResponseLines[responseFilename],
|
||||
|
@ -66,11 +66,11 @@ describe('balena ssh', function () {
|
||||
itSS('should succeed (mocked, device UUID)', async () => {
|
||||
const deviceUUID = 'abc1234';
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetApplication({ notFound: true });
|
||||
api.expectGetDevice({ fullUUID: deviceUUID });
|
||||
api.expectGetDevice({ fullUUID: deviceUUID, isOnline: true });
|
||||
mockedExitCode = 0;
|
||||
|
||||
const { err, out } = await runCommand(`ssh ${deviceUUID}`);
|
||||
|
||||
expect(err).to.be.empty;
|
||||
expect(out).to.be.empty;
|
||||
});
|
||||
@ -90,8 +90,7 @@ describe('balena ssh', function () {
|
||||
'Warning: ssh process exited with non-zero code "255"',
|
||||
];
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetApplication({ notFound: true });
|
||||
api.expectGetDevice({ fullUUID: deviceUUID });
|
||||
api.expectGetDevice({ fullUUID: deviceUUID, isOnline: true });
|
||||
mockedExitCode = 255;
|
||||
|
||||
const { err, out } = await runCommand(`ssh ${deviceUUID}`);
|
||||
@ -114,12 +113,32 @@ describe('balena ssh', function () {
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrLines);
|
||||
expect(out).to.be.empty;
|
||||
});
|
||||
|
||||
it('should fail if device not online (mocked, device UUID)', async () => {
|
||||
const deviceUUID = 'abc1234';
|
||||
const expectedErrLines = ['Device with UUID abc1234 is offline'];
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetDevice({ fullUUID: deviceUUID, isOnline: false });
|
||||
mockedExitCode = 0;
|
||||
|
||||
const { err, out } = await runCommand(`ssh ${deviceUUID}`);
|
||||
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrLines);
|
||||
expect(out).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
/** Check whether the 'ssh' tool (executable) exists in the PATH */
|
||||
async function checkSsh(): Promise<boolean> {
|
||||
const { which } = await import('../../build/utils/helpers');
|
||||
const sshPath = await which('ssh', false);
|
||||
if ((sshPath || '').includes('\\Windows\\System32\\OpenSSH\\ssh')) {
|
||||
// don't use Windows' built-in ssh tool for these test cases
|
||||
// because it messes up with the terminal window such that
|
||||
// "line breaks stop working" (and not even '\033c' fixes it)
|
||||
// and all mocha output gets printed on a single very long line...
|
||||
return false;
|
||||
}
|
||||
return !!sshPath;
|
||||
}
|
||||
|
||||
@ -127,11 +146,13 @@ async function checkSsh(): Promise<boolean> {
|
||||
async function startMockSshServer(): Promise<[Server, number]> {
|
||||
const server = createServer((c) => {
|
||||
// 'connection' listener
|
||||
c.on('end', () => {
|
||||
const handler = (msg: string) => {
|
||||
if (process.env.DEBUG) {
|
||||
console.error('[debug] mock ssh server: client disconnected');
|
||||
console.error(`[debug] mock ssh server: ${msg}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
c.on('error', (err) => handler(err.message));
|
||||
c.on('end', () => handler('client disconnected'));
|
||||
c.end();
|
||||
});
|
||||
server.on('error', (err) => {
|
||||
|
@ -99,6 +99,18 @@ describe('handleError() function', () => {
|
||||
expect(printExpectedErrorMessage.notCalled);
|
||||
});
|
||||
|
||||
it('should process thrown strings correctly', async () => {
|
||||
const error = 'an thrown string';
|
||||
await ErrorsModule.handleError(error);
|
||||
|
||||
expect(printErrorMessage.calledOnce).to.be.true;
|
||||
expect(printErrorMessage.getCall(0).args[0]).to.equal(error);
|
||||
expect(captureException.calledOnce).to.be.true;
|
||||
expect(processExit.calledOnce).to.be.true;
|
||||
|
||||
expect(printExpectedErrorMessage.notCalled);
|
||||
});
|
||||
|
||||
it('should process unexpected errors correctly (debug)', async () => {
|
||||
sandbox.stub(process, 'env').value({ DEBUG: true });
|
||||
|
||||
|
@ -17,6 +17,11 @@
|
||||
},
|
||||
"__id": 43699
|
||||
},
|
||||
"organization": [
|
||||
{
|
||||
"handle": "gh_user"
|
||||
}
|
||||
],
|
||||
"depends_on__application": null,
|
||||
"actor": 3423895,
|
||||
"app_name": "testApp",
|
||||
|
Reference in New Issue
Block a user