Compare commits

...

126 Commits

Author SHA1 Message Date
9b6fb62e4f Update resin-compose-parse from 2.1.3 to 2.3.0
This adds support for Docker compose v2.4

Closes: #2364
Change-type: patch
Signed-off-by: Tomás Migone <tomas@balena.io>
2021-10-19 16:54:42 -03:00
cc60e86507 v12.50.2 2021-10-05 13:32:02 +03:00
bd774e8553 Merge pull request #2357 from balena-io/fix-fleet-rename-error-message
Error message when renaming a fleet now mentions the target name.
2021-10-05 10:27:51 +00:00
c493c33e38 Error message when renaming a fleet now mentions the target name.
Change-type: patch
Signed-off-by: Carlo Miguel F. Cruz <carloc@balena.io>
2021-10-05 17:01:07 +08:00
9487b33144 v12.50.1 2021-09-30 03:51:16 +03:00
befdae1b90 Merge pull request #2353 from balena-io/fix-help-release-finalize
Fix help output for 'release finalize' command
2021-09-30 00:49:35 +00:00
08dfc945f3 Update dependencies (@sentry/node error reporting)
Change-type: patch
2021-09-30 01:12:17 +01:00
8791c2f4e1 Replace mixpanel dependency with simple GET request
Change-type: patch
2021-09-30 01:12:17 +01:00
be306e6a20 Avoid NockMock warnings during standalone executable testing
Change-type: patch
2021-09-30 01:12:17 +01:00
6cfff72c59 Fix help output for 'release finalize' command
Change-type: patch
2021-09-30 01:12:17 +01:00
adae718c2e v12.50.0 2021-09-28 18:07:49 +03:00
132e1a63b2 Merge pull request #2345 from balena-io/add-release-handling
Add support for releases
2021-09-28 15:05:40 +00:00
a18e182ae4 Add support for releases
Signed-off-by: Paul Jonathan <pj@balena.io>
Change-type: minor
2021-09-28 14:28:43 +00:00
e098cdca17 v12.49.0 2021-09-23 23:56:47 +03:00
b42af74983 Merge pull request #2301 from balena-io/add-multiarch-handling
Add multiarch support
2021-09-23 20:54:51 +00:00
8bb211e441 build, deploy: Improve logging of image build messages
Change-type: patch
2021-09-23 16:37:45 +00:00
ffccbfba12 build, deploy: Add support for multiarch base images
Bump version of balena-multibuild to the one that supports multiarch
Remove previous hack to avoid sending platform information to multibuild

Change-type: minor
Signed-off-by: Paul Jonathan <pj@balena.io>
See: https://github.com/balena-io/balena-cli/issues/1508
2021-09-23 16:37:45 +00:00
56c1af50c0 v12.48.15 2021-09-22 17:52:15 +03:00
8b9e3ccdc8 Merge pull request #2346 from balena-io/update-sdk
Update balena-sdk to 15.51.1
2021-09-22 14:50:21 +00:00
de95262f93 Update balena-sdk to 15.51.1
Update balena-sdk from 15.48.0 to 15.51.1

Change-type: patch
2021-09-22 12:46:34 +05:30
ed49938504 v12.48.14 2021-09-22 00:37:13 +03:00
52ad0f6a57 Merge pull request #2347 from balena-io/klutchell/set-zlib-flush
os download: Avoid incomplete os downloads appearing as successful
2021-09-21 21:35:23 +00:00
7f6738c73c os download: Avoid incomplete os downloads appearing as successful
By forcing the zlib flush mode to Z_NO_FLUSH we are more likely to
see an error on image download pipelines vs silent failure and
incomplete files.

This is part of a larger investigation and may be removed in the
future when the root cause of the pipeline failures are identified.

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-09-20 13:28:14 -04:00
88fc3f7714 v12.48.13 2021-09-17 17:01:55 +03:00
1afb29b923 Merge pull request #2340 from balena-io/2339-config-inject-auth
config inject: Remove requirement of being logged in
2021-09-17 13:59:18 +00:00
09a4e8db2d config inject: Remove requirement of being logged in
Change-type: patch
2021-09-16 12:15:53 +01:00
6c81440428 v12.48.12 2021-09-13 18:28:39 +03:00
3eca65ce0d Merge pull request #2333 from balena-io/klutchell/qemu-6-0-0
build/deploy: Update QEMU to 6.0.0 for emulated builds
2021-09-13 15:26:41 +00:00
6319b9dc13 v12.48.11 2021-09-11 01:38:46 +03:00
290acaecbb Merge pull request #2328 from balena-io/825-1018-fix-build-deploy-tag
build, deploy: Fix processing of '--tag' option
2021-09-10 22:36:18 +00:00
305c9045f0 build, deploy: Fix processing of '--tag' option
Change-type: patch
Resolves: #825
Resolves: #1018
2021-09-10 23:11:20 +01:00
b701151769 build/deploy: Update QEMU to 6.0.0 for emulated builds
Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-09-10 13:32:12 +00:00
e03bbb7275 v12.48.10 2021-09-10 14:25:45 +03:00
3fd66c39ae Merge pull request #2331 from balena-io/2330-retry-supervisor-api-request
push: Await and retry supervisor API requests to a local device
2021-09-10 11:23:59 +00:00
b30075a18b push: Await and retry supervisor API requests to a local device
Change-type: patch
2021-09-10 01:44:26 +01:00
a4fc95e99b v12.48.9 2021-09-10 03:44:17 +03:00
63d8e5e6a3 Merge pull request #2332 from balena-io/bump-net-keepalive-v3.0.0
chore: Update net-keepalive dependency (fix CLI packaging errors)
2021-09-10 00:41:55 +00:00
6244af3464 chore: Update net-keepalive dependency (fix CLI packaging errors)
Change-type: patch
2021-09-10 00:20:26 +01:00
8773927b3f v12.48.8 2021-09-09 00:27:52 +03:00
29a3fd40a2 Merge pull request #2326 from balena-io/v13-gitignore-feature-switch
v13 preparations: Add feature switch for removal of '--gitignore' (push, build)
2021-09-08 21:24:42 +00:00
d6faf060e6 v13 preparations: Add feature switch for removal of '--gitignore' (push, build)
Change-type: patch
2021-09-08 18:10:22 +01:00
352fd197b7 v13 preparations: Adjust test cases for 'balena envs'
Change-type: patch
2021-09-08 17:48:16 +01:00
afb6f938b7 v13 preparations: Adjust test cases for 'balena devices'
Change-type: patch
2021-09-08 17:47:40 +01:00
d3adbcdba9 v12.48.7 2021-09-07 18:28:57 +03:00
33fce1f24f Merge pull request #2325 from balena-io/move-reduce-device-type-json
device move: Rely on the device type model to get the compatible apps
2021-09-07 15:26:20 +00:00
ab90a5f150 v12.48.6 2021-09-07 16:55:22 +03:00
a8b2212fed Merge pull request #2324 from balena-io/reduce-device-type-json
preload: Rely on the device type model to get the compatible apps
2021-09-07 13:53:31 +00:00
6bb8df30dd preload: Rely on the device type model to get the compatible apps
Connects-to: #2318
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 14:53:17 +03:00
0327ed766d device move: Improve types & reduce the number of API requests
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 11:43:35 +00:00
1009958340 v12.48.5 2021-09-07 14:12:24 +03:00
5ce17ea70f Merge pull request #2323 from balena-io/no-my_application
preload: Replace my_application query with the SDKs application.getAll()
2021-09-07 11:10:21 +00:00
9c821511b1 device move: Rely on the device type model to get the compatible apps
Connects-to: #2318
Change-type: patch
See: https://www.flowdock.com/app/rulemotion/i-cli/threads/s6x4Z_LoH8IG4PC_YeXMC0TP6v-
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 07:53:02 +00:00
d793335287 preload: Replace my_application query with the SDKs application.getAll()
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 02:09:39 +03:00
dc59b7e4b0 v12.48.4 2021-08-31 04:34:45 +03:00
370b844538 Merge pull request #2316 from balena-io/os-download-no-device-types-v1
os download: Use the hostApps instead of the device-types/v1 endpoint
2021-08-31 01:32:58 +00:00
a8c2724929 v12.48.3 2021-08-31 04:03:14 +03:00
09dd2dd354 Merge pull request #2317 from balena-io/balena-deploy-no-device-types-v1
balena deploy: Retrieve the cpu arch as part of the device type resource
2021-08-31 01:01:30 +00:00
f3ab41841a v12.48.2 2021-08-31 02:23:23 +03:00
3dee30a0fe Merge pull request #2313 from balena-io/install-docs-20210822
Clarify installation instructions
2021-08-30 23:21:40 +00:00
d34073f695 os download: Use the hostApps instead of the device-types/v1 endpoint
Connects-to: #2318
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-08-30 22:32:11 +00:00
24fe6666e4 balena deploy: Retrieve the cpu arch as part of the device type resource
Connects-to: #2318
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-08-30 22:30:16 +00:00
3fd5981085 Clarify installation instructions
Change-type: patch
2021-08-29 02:08:53 +01:00
08ee8643cb v12.48.1 2021-08-27 03:23:23 +03:00
8db36ccec9 Merge pull request #2312 from balena-io/remove-exitWithExpectedError
build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe)
2021-08-27 00:21:24 +00:00
deb3e4c4ac Improve error handling (remove most occurrences of process.exit())
Finally delete the deprecated exitWithExpectedError() function from
'lib/errors.ts'.

Change-type: patch
2021-08-27 00:53:21 +01:00
a8ff21af69 build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe)
Before this commit, `balena build` and `balena deploy` would almost
never respect CTRL-C on Windows (PowerShell, cmd.exe). Now CTRL-C
is respected over a large extent of runtime and, if CTRL-C is hit
while images are being uploaded (`balena deploy`), the release status
is correctly set to 'failed'.

Change-type: patch
2021-08-27 00:53:21 +01:00
4c54d6c171 v12.48.0 2021-08-26 18:38:44 +03:00
83f213c007 Merge pull request #2274 from balena-io/deploy-release-versioning
Add balena.yml handling to `balena deploy` release creation
2021-08-26 15:36:59 +00:00
d0cdc900a2 Add contract contents at release creation time
Change-type: patch
2021-08-26 16:11:23 +01:00
9937b91606 Documentation update with debugging notes
Signed-off-by: Paul Jonathan <pj@balena.io>
2021-08-26 00:34:54 +00:00
972c2470c5 Fix env variable to avoid test failures
Signed-off-by: Paul Jonathan <pj@balena.io>
Change-type: patch
2021-08-25 17:01:22 +00:00
7d568a928b Add balena.yml handling and --draft to balena deploy release creation
This change allows use of a contract and release semver when doing a push,
and is part of the larger feature to use the builder as part of a CI/CD pipeline.

Change-type: minor
Signed-off-by: Paul Jonathan <pj@balena.io>
2021-08-25 17:01:17 +00:00
2331e0a3e5 v12.47.0 2021-08-20 02:15:13 +03:00
cb9b6be24b Merge pull request #2309 from balena-io/warn-deprecation-policy
Add deprecation policy checker and --unsupported global flag
2021-08-19 23:13:25 +00:00
c2d3eee7cc Add deprecation policy checker and --unsupported global flag
Change-type: minor
2021-08-19 23:17:31 +01:00
d8b08f7272 v12.46.2 2021-08-17 02:27:23 +03:00
819bdac354 Merge pull request #2308 from balena-io/bump-sdk-15.48.0
Update dependencies (balena-sdk from v15.36.0 to v15.48.0)
2021-08-16 23:25:22 +00:00
318de8f017 Update dependencies (balena-sdk from v15.36.0 to v15.48.0)
Update balena-sdk from 15.36.0 to 15.48.0

Change-type: patch
2021-08-16 23:33:44 +01:00
2b0341e12a v12.46.1 2021-08-16 18:08:37 +03:00
21f7463607 Merge pull request #2303 from balena-io/preload-custom-dind
preload: Restore support for armv7 with custom preload image
2021-08-16 15:06:32 +00:00
19fd3094d1 preload: Restore support for armv7 with custom preload image
Update balena-preload from 10.4.20 to 10.5.0

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-08-16 09:29:14 -04:00
7c4974f4f5 v12.46.0 2021-08-15 20:09:40 +03:00
3b56ed278e Merge pull request #2270 from balena-io/allow-draft-releases
Add `--draft` option to `balena push`
2021-08-15 17:07:36 +00:00
254ef1c8cf Add --draft option to balena push
This change will allow to build releases as draft and have them being
set as final at a later stage. This change is part of a larger feature towards
using the builder as part of CI/CD pipelines.

Depends-on: https://github.com/balena-io/balena-builder/pull/868
Change-type: minor
2021-08-15 16:43:01 +00:00
d11f49e0f8 v12.45.2 2021-08-14 03:23:04 +03:00
48d7d0ef5e Merge pull request #2305 from balena-io/performResolution-error-handling
push, build: Improve error handling (identify which service failed)
2021-08-14 00:21:11 +00:00
c7bbbc4159 push, build: Improve error handling (identify which service failed)
Change-type: patch
2021-08-13 17:11:48 +01:00
d2fabcaf30 v12.45.1 2021-08-11 17:27:05 +03:00
e137c2aed2 Merge pull request #2307 from balena-io/2306-env-add-app-is-ambiguous
envs, env add: Fix "Application is ambiguous" when using device UUID
2021-08-11 14:25:21 +00:00
58704b08d3 envs, env add: Fix "Application is ambiguous" when using device UUID
Change-type: patch
2021-08-11 02:00:35 +01:00
485a9e944f v12.45.0 2021-08-09 19:35:52 +03:00
1d7a50f007 Merge pull request #2304 from balena-io/fleet-renamathon
Rename applications to fleets (stage 1)
2021-08-09 16:34:07 +00:00
64a44e7a5f Rename applications to fleets (stage 1). See: https://git.io/JRuZr
- Add fleet(s) commands and -f, --fleet flags as aliases to the app(s)
  commands and -a, --app, --application flags.
- Conditionally rename column/row headers and JSON object properties
  from 'application' to 'fleet', with some variations.
- Print warning messages regarding the renaming, provided that stderr
  is attached to an interactive terminal.

Change-type: minor
Resolves: #2302
2021-08-09 12:12:03 +01:00
c3406603db v12.44.29 2021-07-27 16:49:35 +03:00
f1fa187a58 Merge pull request #2300 from balena-io/preload-10-4-10
preload: Fix storage driver detection in balenaOS v2.80.9
2021-07-27 13:47:26 +00:00
6cb2893750 preload: Fix storage driver detection in balenaOS v2.80.9
Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-07-26 12:03:49 -04:00
216172ed4f v12.44.28 2021-07-23 17:55:35 +03:00
3717d8cc0f Merge pull request #2299 from balena-io/catch-BalenaInvalidDeviceType
os download: Improve error message for misspelled device type names
2021-07-23 14:53:22 +00:00
8338e2e933 os download: Improve error message for misspelled device type names
Change-type: patch
2021-07-23 15:10:00 +01:00
918c2e912d v12.44.27 2021-07-23 14:25:16 +03:00
be0622ec80 Merge pull request #2298 from balena-io/etimedout-troubleshooting
docs: Add entry to FAQ/Troubleshooting for ETIMEDOUT with 'balena tunnel'
2021-07-23 11:23:13 +00:00
07eef7bb49 docs: Add entry to FAQ/Troubleshooting for ETIMEDOUT with 'balena tunnel'
Change-type: patch
2021-07-23 11:54:13 +01:00
0892caa155 v12.44.26 2021-07-22 04:31:34 +03:00
fa4e8e7b55 Merge pull request #2296 from balena-io/npm-audit
chore: Update dependencies (balena-lint, oclif, "npm audit fix")
2021-07-22 01:29:20 +00:00
e624726e44 config write: Fix EBUSY error on macOS
Change-type: patch
2021-07-21 23:56:57 +01:00
f914fa2d8a chore: Remove 'umount' dependency (as advised by "npm audit")
Address security advisory https://www.npmjs.com/advisories/1512

Change-type: patch
2021-07-21 23:56:57 +01:00
c8f5542c8a chore: Update oclif
Change-type: patch
2021-07-21 23:56:46 +01:00
a2cad7bf53 chore: Update dependencies ("npm audit fix")
Change-type: patch
2021-07-21 23:56:35 +01:00
3a871a0003 chore: Update balena-lint
Change-type: patch
2021-07-20 18:02:16 +01:00
e552e36f7b v12.44.25 2021-07-20 18:56:54 +03:00
c325f1158e Merge pull request #2295 from balena-io/simplify-run-spinner
Simplify runSpinner api
2021-07-20 15:55:11 +00:00
f79ccc0c95 Simplify runSpinner api
Change-type: patch
2021-07-20 15:46:54 +01:00
1ec8d9a4ca v12.44.24 2021-07-10 00:30:25 +03:00
427b0d9b41 Merge pull request #2291 from balena-io/fix-config-write-101
config write: Fix parsing of 'key' argument with numeric components
2021-07-09 21:28:19 +00:00
cfd790a193 Update 'devDependencies' in package.json
Change-type: patch
2021-07-09 21:34:09 +01:00
36f4c1312b config write: Fix parsing of 'key' argument with numeric components
Change-type: patch
2021-07-09 17:29:21 +01:00
fe7cbf4f74 v12.44.23 2021-06-30 19:26:18 +03:00
4e8b8fe582 Merge pull request #2287 from balena-io/dfunckt-patch-1
Delete CODEOWNERS
2021-06-30 16:23:38 +00:00
2986e6cea3 Delete CODEOWNERS
Change-type: patch
2021-06-30 18:55:44 +03:00
bb6b4b255a v12.44.22 2021-06-24 19:55:41 +03:00
350c4abb96 Merge pull request #2283 from balena-io/2045-sfdisk-spinner
preload: Catch sfdisk errors that result in an endless spinner
2021-06-24 16:53:35 +00:00
fec96b41ee preload: Warn that zip files are only accepted for Intel Edison
Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-06-24 11:04:26 -04:00
1dba5cc7c1 preload: Catch sfdisk errors that result in an endless spinner
Change-type: patch
Changelog-entry: preload: Catch sfdisk errors that result in an endless spinner
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-06-23 12:56:41 -04:00
43c6fe672f v12.44.21 2021-06-23 16:54:41 +03:00
486cae1aaa Merge pull request #2282 from balena-io/cli-author
Update author details in package.json, Windows Programs and Features
2021-06-23 13:52:55 +00:00
4d588e51a7 Update author details in package.json, Windows Programs and Features
Change-type: patch
2021-06-22 00:05:25 +01:00
159 changed files with 10274 additions and 4729 deletions

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @balena-io/balena-cli

View File

@ -32,11 +32,11 @@ Please describe what actually happened instead:
Examples:
```
balena push myApp
balena push myFleet
balena push 192.168.0.12
balena deploy myApp
balena deploy myApp --build
balena build . -a myApp
balena deploy myFleet
balena deploy myFleet --build
balena build . -f myFleet
balena build . -A armv7hf -d raspberrypi3
```
@ -48,7 +48,7 @@ additional information. The `--logs` option reveals additional information for t
```
balena build . --logs
balena deploy myApp --build --logs
balena deploy myFleet --build --logs
```
# Steps to Reproduce the Problem

View File

@ -1,5 +1,4 @@
module.exports = {
spec: 'tests/commands/app/create.spec.ts',
reporter: 'spec',
require: 'ts-node/register/transpile-only',
file: './tests/config-tests',

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,328 @@ 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.50.2 - 2021-10-05
* Error message when renaming a fleet now mentions the target name. [Carlo Miguel F. Cruz]
## 12.50.1 - 2021-09-30
* Update dependencies (@sentry/node error reporting) [Paulo Castro]
* Replace mixpanel dependency with simple GET request [Paulo Castro]
* Avoid NockMock warnings during standalone executable testing [Paulo Castro]
* Fix help output for 'release finalize' command [Paulo Castro]
## 12.50.0 - 2021-09-28
* Add support for releases [Paul Jonathan Zoulin]
## 12.49.0 - 2021-09-23
* build, deploy: Improve logging of image build messages [Paulo Castro]
* build, deploy: Add support for multiarch base images [toochevere]
## 12.48.15 - 2021-09-22
<details>
<summary> Update balena-sdk to 15.51.1 [Nitish Agarwal] </summary>
> ### balena-sdk-15.51.1 - 2021-09-20
>
>
> <details>
> <summary> Update balena-request to v11.4.2 [Kyle Harding] </summary>
>
>> #### balena-request-11.4.2 - 2021-09-20
>>
>> * Allow overriding the default zlib flush setting [Kyle Harding]
>>
> </details>
>
>
> ### balena-sdk-15.51.0 - 2021-09-16
>
> * os.getConfig: Add typings for the provisioningKeyName option [Nitish Agarwal]
>
> ### balena-sdk-15.50.1 - 2021-09-13
>
> * models/os: Always first normalize the device type slug [Thodoris Greasidis]
>
> ### balena-sdk-15.50.0 - 2021-09-10
>
> * Add release.finalize to promote draft releases to final [toochevere]
>
> ### balena-sdk-15.49.1 - 2021-09-10
>
> * typings: Drop the v5-model-only application_type.is_host_os [Thodoris Greasidis]
>
> ### balena-sdk-15.49.0 - 2021-09-06
>
> * os.getSupportedOsUpdateVersions: Use the hostApp releases [Thodoris Greasidis]
> * os.download: Use the hostApp for finding the latest release [Thodoris Greasidis]
>
> ### balena-sdk-15.48.3 - 2021-08-27
>
>
> <details>
> <summary> Update balena-request to 11.4.1 [Kyle Harding] </summary>
>
>> #### balena-request-11.4.1 - 2021-08-27
>>
>> * Allow more lenient gzip decompression [Kyle Harding]
>>
> </details>
>
>
> ### balena-sdk-15.48.2 - 2021-08-27
>
> * Improve hostapp.getAllOsVersions performance & reduce fetched data [Thodoris Greasidis]
>
> ### balena-sdk-15.48.1 - 2021-08-27
>
> * Update typescript to 4.4.2 [Thodoris Greasidis]
>
</details>
## 12.48.14 - 2021-09-20
* os download: Avoid incomplete os downloads appearing as successful [Kyle Harding]
## 12.48.13 - 2021-09-17
* config inject: Remove requirement of being logged in [Paulo Castro]
## 12.48.12 - 2021-09-10
* build/deploy: Update QEMU to 6.0.0 for emulated builds [Kyle Harding]
## 12.48.11 - 2021-09-10
* build, deploy: Fix processing of '--tag' option [Paulo Castro]
## 12.48.10 - 2021-09-10
* push: Await and retry supervisor API requests to a local device [Paulo Castro]
## 12.48.9 - 2021-09-09
* chore: Update net-keepalive dependency (fix CLI packaging errors) [Paulo Castro]
## 12.48.8 - 2021-09-08
* v13 preparations: Add feature switch for removal of '--gitignore' (push, build) [Paulo Castro]
* v13 preparations: Adjust test cases for 'balena envs' [Paulo Castro]
* v13 preparations: Adjust test cases for 'balena devices' [Paulo Castro]
## 12.48.7 - 2021-09-07
* device move: Improve types & reduce the number of API requests [Thodoris Greasidis]
* device move: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
## 12.48.6 - 2021-09-07
* preload: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
## 12.48.5 - 2021-09-07
* preload: Replace my_application query with the SDKs application.getAll() [Thodoris Greasidis]
## 12.48.4 - 2021-08-31
* os download: Use the hostApps instead of the device-types/v1 endpoint [Thodoris Greasidis]
## 12.48.3 - 2021-08-31
* balena deploy: Retrieve the cpu arch as part of the device type resource [Thodoris Greasidis]
## 12.48.2 - 2021-08-30
* Clarify installation instructions [Paulo Castro]
## 12.48.1 - 2021-08-27
* Improve error handling (remove most occurrences of process.exit()) [Paulo Castro]
* build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe) [Paulo Castro]
## 12.48.0 - 2021-08-26
* Add contract contents at release creation time [Paulo Castro]
* Fix env variable to avoid test failures [toochevere]
* Add balena.yml handling and `--draft` to `balena deploy` release creation [toochevere]
## 12.47.0 - 2021-08-19
* Add deprecation policy checker and --unsupported global flag [Paulo Castro]
## 12.46.2 - 2021-08-16
<details>
<summary> Update dependencies (balena-sdk from v15.36.0 to v15.48.0) [Paulo Castro] </summary>
> ### balena-sdk-15.48.0 - 2021-08-15
>
> * Deprecate the release.release_version property [Thodoris Greasidis]
> * typings: Add the release versioning properties [Thodoris Greasidis]
>
> ### balena-sdk-15.47.1 - 2021-08-10
>
> * Run browser tests using the minified browser bundle [Thodoris Greasidis]
> * Move to uglify-js to fix const assignment bug in minified build [Thodoris Greasidis]
>
> ### balena-sdk-15.47.0 - 2021-08-09
>
> * typings: Add the release.is_final & is_finalized_at__date properties [Thodoris Greasidis]
>
> ### balena-sdk-15.46.1 - 2021-07-28
>
> * apiKey.getAll: Return only NamedUserApiKeys for backwards compatibility [Thodoris Greasidis]
>
> ### balena-sdk-15.46.0 - 2021-07-27
>
> * Add email verification & email request methods [Nitish Agarwal]
>
> ### balena-sdk-15.45.0 - 2021-07-26
>
> * Update generateProvisioningKey to include keyName [Nitish Agarwal]
>
> ### balena-sdk-15.44.0 - 2021-07-15
>
> * typings: Add the subscription.is_active computed term [Thodoris Greasidis]
>
> ### balena-sdk-15.43.0 - 2021-07-14
>
> * typings: Add the organization_memebership.effective_seat_role field [Thodoris Greasidis]
>
> ### balena-sdk-15.42.2 - 2021-07-14
>
> * tests: Reduce the number of organizations created [Thodoris Greasidis]
>
> ### balena-sdk-15.42.1 - 2021-07-13
>
> * tests/api-key: Fix a race condition in the apiKey.create() tests [Thodoris Greasidis]
> * Convert the apiKey tests to async-await [Thodoris Greasidis]
>
> ### balena-sdk-15.42.0 - 2021-07-13
>
> * models/apiKey: Add getProvisioningApiKeysByApplication() method [Nitish Agarwal]
>
> ### balena-sdk-15.41.1 - 2021-06-30
>
> * Delete CODEOWNERS [Thodoris Greasidis]
>
> ### balena-sdk-15.41.0 - 2021-06-21
>
> * Add organization__has_private_access_to__device_type typings [Thodoris Greasidis]
> * typings: Add organization.has_past_due_invoice_since__date [Thodoris Greasidis]
>
> ### balena-sdk-15.40.0 - 2021-06-09
>
> * Add getAllNamedUserApiKeys() in the apiKey model [Thodoris Greasidis]
>
> ### balena-sdk-15.39.4 - 2021-06-08
>
> * Add missing modified_at in device type [JSReds]
>
> ### balena-sdk-15.39.3 - 2021-06-08
>
> * Fix lint with new linter version [JSReds]
>
> ### balena-sdk-15.39.2 - 2021-05-27
>
> * Update TypeScript to v4.3.2 [Thodoris Greasidis]
>
> ### balena-sdk-15.39.1 - 2021-05-24
>
> * Update balena-lint to v6 [Thodoris Greasidis]
>
> ### balena-sdk-15.39.0 - 2021-05-24
>
> * Add public device types [Tomás Migone]
>
> ### balena-sdk-15.38.0 - 2021-05-20
>
> * models/billing: Add changePlan method [Thodoris Greasidis]
> * Update DOCUMENTATION about getAllWithDeviceServiceDetails deprecation [Thodoris Greasidis]
>
> ### balena-sdk-15.37.0 - 2021-05-17
>
> * Add public organization types [JSReds]
>
</details>
## 12.46.1 - 2021-08-16
<details>
<summary> preload: Restore support for armv7 with custom preload image [Kyle Harding] </summary>
> ### balena-preload-10.5.0 - 2021-08-04
>
> * Remove mutually exclusive args from sfdisk [Kyle Harding]
> * Explicitly disable tls to avoid startup delays [Kyle Harding]
> * Use custom dind image based on alpine [Kyle Harding]
>
</details>
## 12.46.0 - 2021-08-15
* Add `--draft` option to `balena push` [Felipe Lalanne]
## 12.45.2 - 2021-08-13
* push, build: Improve error handling (identify which service failed) [Paulo Castro]
## 12.45.1 - 2021-08-11
* envs, env add: Fix "Application is ambiguous" when using device UUID [Paulo Castro]
## 12.45.0 - 2021-08-09
* Rename applications to fleets (stage 1). See: https://git.io/JRuZr [Paulo Castro]
## 12.44.29 - 2021-07-26
* preload: Fix storage driver detection in balenaOS v2.80.9 [Kyle Harding]
## 12.44.28 - 2021-07-23
* os download: Improve error message for misspelled device type names [Paulo Castro]
## 12.44.27 - 2021-07-23
* docs: Add entry to FAQ/Troubleshooting for ETIMEDOUT with 'balena tunnel' [Paulo Castro]
## 12.44.26 - 2021-07-21
* config write: Fix EBUSY error on macOS [Paulo Castro]
* chore: Remove 'umount' dependency (as advised by "npm audit") [Paulo Castro]
* chore: Update oclif [Paulo Castro]
* chore: Update dependencies ("npm audit fix") [Paulo Castro]
* chore: Update balena-lint [Paulo Castro]
## 12.44.25 - 2021-07-20
* Simplify runSpinner api [Pagan Gazzard]
## 12.44.24 - 2021-07-09
* Update 'devDependencies' in package.json [Paulo Castro]
* config write: Fix parsing of 'key' argument with numeric components [Paulo Castro]
## 12.44.23 - 2021-06-30
* Delete CODEOWNERS [dfunckt]
## 12.44.22 - 2021-06-24
* preload: Warn that zip files are only accepted for Intel Edison [Kyle Harding]
* preload: Catch sfdisk errors that result in an endless spinner [Kyle Harding]
## 12.44.21 - 2021-06-22
* Update author details in package.json, Windows Programs and Features [Paulo Castro]
## 12.44.20 - 2021-06-14
* devices supported: Use new DeviceType data model as source of truth [Paulo Castro]

View File

@ -259,3 +259,12 @@ gotchas to bear in mind:
`node_modules/balena-sdk/node_modules/balena-errors`
In the case of subclasses of `TypedError`, a string comparison may be used instead:
`error.name === 'BalenaApplicationNotFound'`
## Further debugging notes
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
replace: `spec: 'tests/**/*.spec.ts',`
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`

View File

@ -1,6 +1,10 @@
# balena CLI Installation Instructions for Linux
These instructions are suitable for most Linux distributions on Intel x86, except notably for **Linux Alpine** or **Busybox**. For these distros or for the ARM architecture, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation) method.
These instructions are suitable for most Linux distributions on Intel x86, such as
Ubuntu, Debian, Fedora, Arch Linux and other glibc-based distributions.
For the ARM architecture and for Linux distributions not based on glibc, such as
Alpine Linux, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation)
method.
Selected operating system: **Linux**

View File

@ -10,16 +10,14 @@ Selected operating system: **macOS**
Look for a file name that ends with "-installer.pkg":
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
2. Double click the downloaded file to run the installer. After the installation completes,
close and re-open any open [command
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
windows (so that the changes made by the installer to the PATH environment variable can take
effect).
2. Double click on the downloaded file to run the installer and follow the installer's
instructions.
3. Check that the installation was successful by running the following commands on a
command terminal:
* `balena version` - should print the CLI's version
* `balena help` - should print a list of available commands
3. Check that the installation was successful:
- [Open the Terminal
app](https://support.apple.com/en-gb/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac).
- On the terminal prompt, type `balena version` and hit Enter. It should display
the version of the balena CLI that you have installed.
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
and `preload` commands may require additional software to be installed, as described

View File

@ -10,16 +10,14 @@ Selected operating system: **Windows**
Look for a file name that ends with "-installer.exe":
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
2. Double click the downloaded file to run the installer. After the installation completes,
close and re-open any open [command
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
windows (so that the changes made by the installer to the PATH environment variable can take
effect).
2. Double click on the downloaded file to run the installer and follow the installer's
instructions.
3. Check that the installation was successful by running the following commands on a
command terminal:
* `balena version` - should print the CLI's version
* `balena help` - should print a list of available commands
3. Check that the installation was successful:
- Click on the Windows Start Menu, type PowerShell, and then click
on Windows PowerShell.
- On the command prompt, type `balena version` and hit Enter. It should display
the version of the balena CLI that you have installed.
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

View File

@ -57,7 +57,7 @@ guide](https://docs.docker.com/compose/completion/) for system setup instruction
## Logging in
Several CLI commands require access to your balenaCloud account, for example in order to push a
new release to your application. Those commands require creating a CLI login session by running:
new release to your fleet. Those commands require creating a CLI login session by running:
```sh
$ balena login
@ -156,14 +156,18 @@ of major, minor and patch version releases.
The latest release of a major version of the balena CLI will remain compatible with
the balenaCloud backend services for at least one year from the date when the
following major version is released. For example, balena CLI v10.17.5, as the
latest v10 release, would remain compatible with the balenaCloud backend for one
year from the date when v11.0.0 is released.
following major version is released. For example, balena CLI v11.36.0, as the
latest v11 release, would remain compatible with the balenaCloud backend for one
year from the date when v12.0.0 was released.
At the end of this period, the older major version is considered deprecated and
some of the functionality that depends on balenaCloud services may stop working
at any time.
Users are encouraged to regularly update the balena CLI to the latest version.
Half way through to that period (6 months after the release of the next major
version), older major versions of the balena CLI will start printing a deprecation
warning message when it is used interactively (when `stderr` is attached to a TTY
device file). At the end of that period, older major versions will exit with an
error message unless the `--unsupported` flag is used. This behavior was
introduced in CLI version 12.47.0 and is also documented by `balena help`.
To take advantage of the latest backend features and ensure compatibility, users
are encouraged to regularly update the balena CLI to the latest version.
## Contributing (including editing documentation files)

View File

@ -31,6 +31,11 @@ command again.
Check whether the SD card is locked (a physical switch on the side of the card).
## I get `connect ETIMEDOUT` with `balena tunnel`
Please update the CLI to the latest version. This issue was fixed in v12.38.5.
For more details, see: https://github.com/balena-io/balena-cli/issues/2172
## I get EINVAL errors on Cygwin
The errors may look something like this:

View File

@ -22,25 +22,26 @@ import * as archiver from 'archiver';
import * as Bluebird from 'bluebird';
import { execFile } from 'child_process';
import * as filehound from 'filehound';
import { Stats } from 'fs';
import * as fs from 'fs-extra';
import * as klaw from 'klaw';
import * as _ from 'lodash';
import * as path from 'path';
import * as rimraf from 'rimraf';
import * as semver from 'semver';
import * as util from 'util';
import * as klaw from 'klaw';
import { Stats } from 'fs';
import { promisify } from 'util';
import { stripIndent } from '../lib/utils/lazy';
import { stripIndent } from '../build/utils/lazy';
import {
diffLines,
getSubprocessStdout,
loadPackageJson,
ROOT,
StdOutTap,
whichSpawn,
} from './utils';
const execFileAsync = promisify(execFile);
export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version;
const arch = process.arch;
@ -246,7 +247,17 @@ async function testPkg() {
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
// Run `balena version -j`, parse its stdout as JSON, and check that the
// reported Node.js major version matches semver.major(process.version)
const stdout = await getSubprocessStdout(pkgBalenaPath, ['version', '-j']);
let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
'version',
'-j',
]);
const { filterCliOutputForTests } = await import('../tests/helpers');
const filtered = filterCliOutputForTests({
err: stderr.split(/\r?\n/),
out: stdout.split(/\r?\n/),
});
stdout = filtered.out.join('\n');
stderr = filtered.err.join('\n');
let pkgNodeVersion = '';
let pkgNodeMajorVersion = 0;
try {
@ -263,6 +274,10 @@ async function testPkg() {
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
);
}
if (filtered.err.length > 0) {
const err = filtered.err.join('\n');
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
}
console.log('Success! (standalone package test successful)');
}
@ -411,8 +426,6 @@ async function renameInstallerFiles() {
async function signWindowsInstaller() {
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
const exeName = renamedOclifInstallers[process.platform];
const execFileAsync = util.promisify<string, string[], void>(execFile);
console.log(`Signing installer "${exeName}"`);
await execFileAsync(MSYS2_BASH, [
'sign-exe.sh',

View File

@ -34,15 +34,22 @@ const capitanoDoc = {
files: ['build/commands/api-key/generate.js'],
},
{
title: 'Application',
title: 'Fleet',
files: [
'build/commands/apps.js',
'build/commands/fleets.js',
'build/commands/app/index.js',
'build/commands/fleet/index.js',
'build/commands/app/create.js',
'build/commands/fleet/create.js',
'build/commands/app/purge.js',
'build/commands/fleet/purge.js',
'build/commands/app/rename.js',
'build/commands/fleet/rename.js',
'build/commands/app/restart.js',
'build/commands/fleet/restart.js',
'build/commands/app/rm.js',
'build/commands/fleet/rm.js',
],
},
{
@ -75,6 +82,14 @@ const capitanoDoc = {
'build/commands/device/shutdown.js',
],
},
{
title: 'Releases',
files: [
'build/commands/releases.js',
'build/commands/release/index.js',
'build/commands/release/finalize.js',
],
},
{
title: 'Environment Variables',
files: [

View File

@ -58,7 +58,7 @@ class FakeHelpCommand {
examples = [
'$ balena help',
'$ balena help apps',
'$ balena help login',
'$ balena help os download',
];
@ -86,7 +86,7 @@ function importOclifCommands(jsFilename: string): OclifCommand[] {
const command: OclifCommand =
jsFilename === 'help'
? ((new FakeHelpCommand() as unknown) as OclifCommand)
? (new FakeHelpCommand() as unknown as OclifCommand)
: (require(path.join(process.cwd(), jsFilename)).default as OclifCommand);
return [command];
@ -101,7 +101,7 @@ async function printMarkdown() {
console.log(await renderMarkdown());
} catch (error) {
console.error(error);
process.exit(1);
process.exitCode = 1;
}
}

View File

@ -41,17 +41,25 @@ function checkNpmVersion() {
// the reason is that it would unnecessarily prevent end users from
// using npm v6.4.1 that ships with Node 8. (It is OK for the
// shrinkwrap file to get damaged if it is not going to be reused.)
console.error(`\
-------------------------------------------------------------------------------
throw new Error(`\
-----------------------------------------------------------------------------
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
At this point, however, your 'npm-shrinkwrap.json' file has already been
damaged. Please revert it to the master branch state with a command such as:
"git checkout master -- npm-shrinkwrap.json"
Then re-run "npm install" using npm version ${requiredVersion} or later.
-------------------------------------------------------------------------------`);
process.exit(1);
-----------------------------------------------------------------------------`);
}
}
checkNpmVersion();
function main() {
try {
checkNpmVersion();
} catch (e) {
console.error(e.message || e);
process.exitCode = 1;
}
}
main();

View File

@ -54,17 +54,18 @@ export async function release() {
try {
await createGitHubRelease();
} catch (err) {
console.error('Release failed');
console.error(err);
process.exit(1);
throw new Error(`Error creating GitHub release:\n${err}`);
}
}
/** Return a cached Octokit instance, creating a new one as needed. */
const getOctokit = _.once(function () {
const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin(
(require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling'))
.throttling,
const Octokit = (
require('@octokit/rest') as typeof import('@octokit/rest')
).Octokit.plugin(
(
require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling')
).throttling,
);
return new Octokit({
auth: GITHUB_TOKEN,
@ -110,7 +111,8 @@ function getPageNumbers(
if (!response.headers.link) {
return res;
}
const parse = require('parse-link-header') as typeof import('parse-link-header');
const parse =
require('parse-link-header') as typeof import('parse-link-header');
const parsed = parse(response.headers.link);
if (parsed == null) {
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
@ -158,11 +160,14 @@ async function updateGitHubReleaseDescriptions(
per_page: perPage,
});
let errCount = 0;
for await (const response of octokit.paginate.iterator(options)) {
const { page: thisPage, pages: totalPages, ordinal } = getPageNumbers(
response,
perPage,
);
type Release =
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
for await (const response of octokit.paginate.iterator<Release>(options)) {
const {
page: thisPage,
pages: totalPages,
ordinal,
} = getPageNumbers(response, perPage);
let i = 0;
for (const cliRelease of response.data) {
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;

View File

@ -35,11 +35,6 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
? ''
: '1';
function exitWithError(error: Error | string): never {
console.error(`Error: ${error}`);
process.exit(1);
}
/**
* Trivial command-line parser. Check whether the command-line argument is one
* of the following strings, then call the appropriate functions:
@ -49,12 +44,12 @@ function exitWithError(error: Error | string): never {
*
* @param args Arguments to parse (default is process.argv.slice(2))
*/
export async function run(args?: string[]) {
async function parse(args?: string[]) {
args = args || process.argv.slice(2);
console.log(`automation/run.ts process.argv=[${process.argv}]\n`);
console.log(`automation/run.ts args=[${args}]`);
console.error(`[debug] automation/run.ts process.argv=[${process.argv}]`);
console.error(`[debug] automation/run.ts args=[${args}]`);
if (_.isEmpty(args)) {
return exitWithError('missing command-line arguments');
throw new Error('missing command-line arguments');
}
const commands: { [cmd: string]: () => void | Promise<void> } = {
'build:installer': buildOclifInstaller,
@ -66,7 +61,7 @@ export async function run(args?: string[]) {
};
for (const arg of args) {
if (!commands.hasOwnProperty(arg)) {
return exitWithError(`command unknown: ${arg}`);
throw new Error(`command unknown: ${arg}`);
}
}
@ -90,9 +85,22 @@ export async function run(args?: string[]) {
const cmdFunc = commands[arg];
await cmdFunc();
} catch (err) {
return exitWithError(`"${arg}": ${err}`);
if (typeof err === 'object') {
err.message = `"${arg}": ${err.message}`;
}
throw err;
}
}
}
/** See jsdoc for parse() function above */
export async function run(args?: string[]) {
try {
await parse(args);
} catch (e) {
console.error(e.message ? `Error: ${e.message}` : e);
process.exitCode = 1;
}
}
run();

View File

@ -11,8 +11,7 @@ const validateChangeType = (maybeChangeType: string = 'minor') => {
case 'major':
return maybeChangeType;
default:
console.error(`Invalid change type: '${maybeChangeType}'`);
return process.exit(1);
throw new Error(`Invalid change type: '${maybeChangeType}'`);
}
};
@ -65,24 +64,17 @@ const getUpstreams = async () => {
return upstream;
};
const printUsage = (upstreams: Upstream[], upstreamName: string) => {
console.error(
`
const getUsage = (upstreams: Upstream[], upstreamName: string) => `
Usage: npm run update ${upstreamName} $version [$changeType=minor]
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
`,
);
return process.exit(1);
};
`;
// TODO: Drop the wrapper function once we move to TS 3.8,
// which will support top level await.
async function main() {
async function $main() {
const upstreams = await getUpstreams();
if (process.argv.length < 3) {
return printUsage(upstreams, '$upstreamName');
throw new Error(getUsage(upstreams, '$upstreamName'));
}
const upstreamName = process.argv[2];
@ -90,16 +82,15 @@ async function main() {
const upstream = upstreams.find((v) => v.repo === upstreamName);
if (!upstream) {
console.error(
throw new Error(
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
.map(({ repo }) => repo)
.join(', ')}`,
);
return process.exit(1);
}
if (process.argv.length < 4) {
printUsage(upstreams, upstreamName);
throw new Error(getUsage(upstreams, upstreamName));
}
const packageName = upstream.module || upstream.repo;
@ -108,8 +99,7 @@ async function main() {
await run(`npm install ${packageName}@${process.argv[3]}`);
const newVersion = await getVersion(packageName);
if (newVersion === oldVersion) {
console.error(`Already on version '${newVersion}'`);
return process.exit(1);
throw new Error(`Already on version '${newVersion}'`);
}
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
@ -137,4 +127,13 @@ async function main() {
);
}
async function main() {
try {
await $main();
} catch (e) {
console.error(e);
process.exitCode = 1;
}
}
main();

View File

@ -21,22 +21,6 @@ import * as path from 'path';
export const ROOT = path.join(__dirname, '..');
const nodeEngineWarn = `\
------------------------------------------------------------------------------
Warning: Node version "v14.x.x" does not match required versions ">=10.20.0 <13.0.0".
This may cause unexpected behavior. To upgrade Node, visit:
https://nodejs.org/en/download/
------------------------------------------------------------------------------
`;
const nodeEngineWarnArray = nodeEngineWarn.split('\n').filter((l) => l);
export function matchesNodeEngineVersionWarn(line: string) {
line = line.replace(/"v14\.\d{1,3}\.\d{1,3}"/, '"v14.x.x"');
return (
line === nodeEngineWarn || nodeEngineWarnArray.includes(line.trimEnd())
);
}
/** Tap and buffer this process' stdout and stderr */
export class StdOutTap {
public stdoutBuf: string[] = [];
@ -104,60 +88,6 @@ export function loadPackageJson() {
return require(path.join(ROOT, 'package.json'));
}
/**
* Run the executable at execPath as a child process, and resolve a promise
* to the executable's stdout output as a string. Reject the promise if
* anything is printed to stderr, or if the child process exits with a
* non-zero exit code.
* @param execPath Executable path
* @param args Command-line argument for the executable
*/
export async function getSubprocessStdout(
execPath: string,
args: string[],
): Promise<string> {
const child = spawn(execPath, args);
return new Promise((resolve, reject) => {
let stdout = '';
child.stdout.on('error', reject);
child.stderr.on('error', reject);
child.stdout.on('data', (data: Buffer) => {
try {
stdout = data.toString();
} catch (err) {
reject(err);
}
});
child.stderr.on('data', (data: Buffer) => {
try {
const stderr = data.toString();
// ignore any debug lines, but ensure that we parse
// every line provided to the stderr stream
const lines = _.filter(
stderr.trim().split(/\r?\n/),
(line) =>
!line.startsWith('[debug]') && !matchesNodeEngineVersionWarn(line),
);
if (lines.length > 0) {
reject(
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
);
}
} catch (err) {
reject(err);
}
});
child.on('exit', (code: number) => {
if (code) {
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
} else {
resolve(stdout);
}
});
});
}
/**
* Error handling wrapper around the npm `which` package:
* "Like the unix which utility. Finds the first instance of a specified

View File

@ -8,7 +8,7 @@ _balena() {
local context state line curcontext="$curcontext"
# Valid top-level completions
main_commands=( apps build deploy envs join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env internal key key local os tag util )
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util )
# Sub-completions
api_key_cmds=( generate )
app_cmds=( create purge rename restart rm )
@ -16,10 +16,12 @@ _balena() {
device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown )
devices_cmds=( supported )
env_cmds=( add rename rm )
fleet_cmds=( create purge rename restart rm )
internal_cmds=( osinit )
key_cmds=( add rm )
local_cmds=( configure flash )
os_cmds=( build-config configure download initialize versions )
release_cmds=( finalize )
tag_cmds=( rm set )
@ -57,6 +59,9 @@ _balena_sec_cmds() {
"env")
_describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0
;;
"fleet")
_describe -t fleet_cmds 'fleet_cmd' fleet_cmds "$@" && ret=0
;;
"internal")
_describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0
;;
@ -69,6 +74,9 @@ _balena_sec_cmds() {
"os")
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
;;
"release")
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
;;
"tag")
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
;;

View File

@ -7,7 +7,7 @@ _balena_complete()
local cur prev
# Valid top-level completions
main_commands="apps build deploy envs join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env internal key key local os tag util"
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util"
# Sub-completions
api_key_cmds="generate"
app_cmds="create purge rename restart rm"
@ -15,10 +15,12 @@ _balena_complete()
device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown"
devices_cmds="supported"
env_cmds="add rename rm"
fleet_cmds="create purge rename restart rm"
internal_cmds="osinit"
key_cmds="add rm"
local_cmds="configure flash"
os_cmds="build-config configure download initialize versions"
release_cmds="finalize"
tag_cmds="rm set"
@ -51,6 +53,9 @@ _balena_complete()
env)
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
;;
fleet)
COMPREPLY=( $(compgen -W "$fleet_cmds" -- $cur) )
;;
internal)
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
;;
@ -63,6 +68,9 @@ _balena_complete()
os)
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
;;
release)
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
;;
tag)
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
;;

View File

@ -7,7 +7,7 @@ It requires collecting some preliminary information _once_.
The final command to provision the device looks like this:
```bash
balena device init --app APP_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
balena device init --fleet FLEET_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
```
@ -24,7 +24,7 @@ But before you can run it you need to collect the parameters and build the confi
```
and find the _slug_ for your target device type, like _raspberrypi3_.
1. `APP_ID`. Create an application (`balena app create APP_NAME --type DEVICE_TYPE`) or find an existing one (`balena apps`) and notice its ID.
1. `FLEET_ID`. Create a fleet (`balena fleet create FLEET_NAME --type DEVICE_TYPE`) or find an existing one (`balena fleets`) and notice its ID.
1. `OS_VERSION`. Run
```bash

File diff suppressed because it is too large Load Diff

View File

@ -16,8 +16,14 @@
*/
import * as packageJSON from '../package.json';
import {
AppOptions,
checkDeletedCommand,
preparseArgs,
unsupportedFlag,
} from './preparser';
import { CliSettings } from './utils/bootstrap';
import { onceAsync, stripIndent } from './utils/lazy';
import { onceAsync } from './utils/lazy';
/**
* Sentry.io setup
@ -27,6 +33,7 @@ export const setupSentry = onceAsync(async () => {
const config = await import('./config');
const Sentry = await import('@sentry/node');
Sentry.init({
autoSessionTracking: false,
dsn: config.sentryDsn,
release: packageJSON.version,
});
@ -43,13 +50,8 @@ export const setupSentry = onceAsync(async () => {
async function checkNodeVersion() {
const validNodeVersions = packageJSON.engines.node;
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
console.warn(stripIndent`
------------------------------------------------------------------------------
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
This may cause unexpected behavior. To upgrade Node, visit:
https://nodejs.org/en/download/
------------------------------------------------------------------------------
`);
const { getNodeEngineVersionWarn } = await import('./utils/messages');
console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
}
}
@ -93,10 +95,20 @@ async function init() {
}
/** Execute the oclif parser and the CLI command. */
async function oclifRun(
command: string[],
options: import('./preparser').AppOptions,
) {
async function oclifRun(command: string[], options: AppOptions) {
let deprecationPromise: Promise<void>;
// check and enforce the CLI's deprecation policy
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
deprecationPromise = Promise.resolve();
} else {
const { DeprecationChecker } = await import('./deprecation');
const deprecationChecker = new DeprecationChecker(packageJSON.version);
// warnAndAbortIfDeprecated uses previously cached data only
await deprecationChecker.warnAndAbortIfDeprecated();
// checkForNewReleasesIfNeeded may query the npm registry
deprecationPromise = deprecationChecker.checkForNewReleasesIfNeeded();
}
const runPromise = (async function (shouldFlush: boolean) {
const { CustomMain } = await import('./utils/oclif-utils');
let isEEXIT = false;
@ -130,14 +142,12 @@ async function oclifRun(
})(!options.noFlush);
const { trackPromise } = await import('./hooks/prerun/track');
await Promise.all([trackPromise, runPromise]);
await Promise.all([trackPromise, deprecationPromise, runPromise]);
}
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
export async function run(
cliArgs = process.argv,
options: import('./preparser').AppOptions = {},
) {
export async function run(cliArgs = process.argv, options: AppOptions = {}) {
try {
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
normalizeEnvVars();
@ -150,8 +160,6 @@ export async function run(
await init();
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
// Look for commands that have been removed and if so, exit with a notice
checkDeletedCommand(cliArgs.slice(2));

View File

@ -59,14 +59,15 @@ export class LoginServer extends EventEmitter {
app.set('views', path.join(__dirname, 'pages'));
this.server = await new Promise<import('net').Server>((resolve, reject) => {
const server = app.listen(port, host, (err: Error) => {
const callback = (err: Error) => {
if (err) {
this.emit('error', err);
reject(err);
} else {
resolve(server);
}
});
};
const server = app.listen(port, host, callback as any);
server.on('connection', (socket) => this.serverSockets.push(socket));
});

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,11 +16,14 @@
*/
import { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import type { Application } 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 type { Application } from 'balena-sdk';
import { appToFleetCmdMsg, warnify } from '../../utils/messages';
interface FlagsDef {
organization?: string;
@ -32,18 +35,18 @@ interface ArgsDef {
name: string;
}
export default class AppCreateCmd extends Command {
export class FleetCreateCmd extends Command {
public static description = stripIndent`
Create an application.
Create a fleet.
Create a new balena application.
Create a new balena fleet.
You can specify the organization the application should belong to using
You can specify the organization the fleet 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.
The application's default device type is specified with the \`--type\` option.
The fleet's default device type is specified with the \`--type\` option.
The \`balena devices supported\` command can be used to list the available
device types.
@ -55,41 +58,39 @@ export default class AppCreateCmd extends Command {
`;
public static examples = [
'$ balena app create MyApp',
'$ balena app create MyApp --organization mmyorg',
'$ balena app create MyApp -o myorg --type raspberry-pi',
'$ balena fleet create MyFleet',
'$ balena fleet create MyFleet --organization mmyorg',
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
];
public static args = [
{
name: 'name',
description: 'application name',
description: 'fleet name',
required: true,
},
];
public static usage = 'app create <name>';
public static usage = 'fleet create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description:
'handle of the organization the application should belong to',
description: 'handle of the organization the fleet should belong to',
}),
type: flags.string({
char: 't',
description:
'application device type (Check available types with `balena devices supported`)',
'fleet device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
AppCreateCmd,
);
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params, flags: options } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCreateCmd);
// Ascertain device type
const deviceType =
@ -112,12 +113,12 @@ export default class AppCreateCmd extends Command {
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 in organization "${organization}".`,
`Error: fleet "${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}".`,
`Error: You are not authorized to create fleets in organization "${organization}".`,
);
}
@ -128,8 +129,8 @@ export default class AppCreateCmd extends Command {
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})`,
? `Fleet created: slug "${application.slug}", device type "${deviceType}"`
: `Fleet created: ${application.slug} (${deviceType}, id ${application.id})`,
);
}
@ -150,3 +151,31 @@ export default class AppCreateCmd extends Command {
}
}
}
export default class AppCreateCmd extends FleetCreateCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet create' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet create'
`;
public static examples = [];
public static usage = 'app create <name>';
public static args = FleetCreateCmd.args;
public static flags = FleetCreateCmd.flags;
public static authenticated = FleetCreateCmd.authenticated;
public static primary = FleetCreateCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCreateCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,35 +15,44 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import type { Release } from 'balena-sdk';
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';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
application: string;
fleet: string;
}
export default class AppCmd extends Command {
export class FleetCmd extends Command {
public static description = stripIndent`
Display information about a single application.
Display information about a single fleet.
Display detailed information about a single balena application.
Display detailed information about a single fleet.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = ['$ balena app MyApp', '$ balena app myorg/myapp'];
public static examples = [
'$ balena fleet MyFleet',
'$ balena fleet myorg/myfleet',
];
public static args = [ca.applicationRequired];
public static args = [ca.fleetRequired];
public static usage = 'app <nameOrSlug>';
public static usage = 'fleet <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
@ -52,21 +61,18 @@ export default class AppCmd extends Command {
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppCmd);
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCmd);
const { getApplication } = await import('../../utils/sdk');
const application = (await getApplication(
getBalenaSdk(),
params.application,
{
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
},
const application = (await getApplication(getBalenaSdk(), params.fleet, {
$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;
@ -88,3 +94,31 @@ export default class AppCmd extends Command {
);
}
}
export default class AppCmd extends FleetCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet'
`;
public static examples = [];
public static usage = 'app <fleet>';
public static args = FleetCmd.args;
public static flags = FleetCmd.flags;
public static authenticated = FleetCmd.authenticated;
public static primary = FleetCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -15,39 +15,45 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
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';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
application: string;
fleet: string;
}
export default class AppPurgeCmd extends Command {
export class FleetPurgeCmd extends Command {
public static description = stripIndent`
Purge data from an application.
Purge data from a fleet.
Purge data from all devices belonging to an application.
This will clear the application's /data directory.
Purge data from all devices belonging to a fleet.
This will clear the fleet's '/data' directory.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena app purge MyApp',
'$ balena app purge myorg/myapp',
'$ balena fleet purge MyFleet',
'$ balena fleet purge myorg/myfleet',
];
public static args = [ca.applicationRequired];
public static args = [ca.fleetRequired];
public static usage = 'app purge <application>';
public static usage = 'fleet purge <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
@ -55,8 +61,9 @@ export default class AppPurgeCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
const { getApplication } = await import('../../utils/sdk');
@ -64,7 +71,7 @@ export default class AppPurgeCmd extends Command {
// balena.models.application.purge only accepts a numeric id
// so we must first fetch the app to get it's id,
const application = await getApplication(balena, params.application);
const application = await getApplication(balena, params.fleet);
try {
await balena.models.application.purge(application.id);
@ -78,3 +85,31 @@ export default class AppPurgeCmd extends Command {
}
}
}
export default class AppPurgeCmd extends FleetPurgeCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet purge' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet purge'
`;
public static examples = [];
public static usage = 'app purge <fleet>';
public static args = FleetPurgeCmd.args;
public static flags = FleetPurgeCmd.flags;
public static authenticated = FleetPurgeCmd.authenticated;
public static primary = FleetPurgeCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -15,28 +15,34 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import type { ApplicationType } from 'balena-sdk';
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 { applicationIdInfo } from '../../utils/messages';
import type { ApplicationType } from 'balena-sdk';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
application: string;
fleet: string;
newName?: string;
}
export default class AppRenameCmd extends Command {
export class FleetRenameCmd extends Command {
public static description = stripIndent`
Rename an application.
Rename a fleet.
Rename an application.
Rename a fleet.
Note, if the \`newName\` parameter is omitted, it will be
prompted for interactively.
@ -45,20 +51,20 @@ export default class AppRenameCmd extends Command {
`;
public static examples = [
'$ balena app rename OldName',
'$ balena app rename OldName NewName',
'$ balena app rename myorg/oldname NewName',
'$ balena fleet rename OldName',
'$ balena fleet rename OldName NewName',
'$ balena fleet rename myorg/oldname NewName',
];
public static args = [
ca.applicationRequired,
ca.fleetRequired,
{
name: 'newName',
description: 'the new name for the application',
description: 'the new name for the fleet',
},
];
public static usage = 'app rename <application> [newName]';
public static usage = 'fleet rename <fleet> [newName]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
@ -66,8 +72,9 @@ export default class AppRenameCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
const { validateApplicationName } = await import('../../utils/validation');
const { ExpectedError } = await import('../../errors');
@ -76,7 +83,7 @@ export default class AppRenameCmd extends Command {
// 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, {
const application = await getApplication(balena, params.fleet, {
$expand: {
application_type: {
$select: ['is_legacy'],
@ -86,16 +93,14 @@ export default class AppRenameCmd extends Command {
// Check app exists
if (!application) {
throw new ExpectedError(
'Error: application ${params.nameOrSlug} not found.',
);
throw new ExpectedError(`Error: fleet ${params.fleet} not found.`);
}
// 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.`,
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
);
}
@ -103,7 +108,7 @@ export default class AppRenameCmd extends Command {
const newName =
params.newName ||
(await getCliForm().ask({
message: 'Please enter the new name for this application:',
message: 'Please enter the new name for this fleet:',
type: 'input',
validate: validateApplicationName,
})) ||
@ -115,9 +120,7 @@ export default class AppRenameCmd extends Command {
} catch (e) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
if ((e.message || '').toLowerCase().includes('unique')) {
throw new ExpectedError(
`Error: application ${params.application} already exists.`,
);
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
}
throw e;
}
@ -128,7 +131,7 @@ export default class AppRenameCmd extends Command {
);
// Output result
console.log(`Application renamed`);
console.log(`Fleet renamed`);
console.log('From:');
console.log(`\tname: ${application.app_name}`);
console.log(`\tslug: ${application.slug}`);
@ -137,3 +140,31 @@ export default class AppRenameCmd extends Command {
console.log(`\tslug: ${renamedApplication.slug}`);
}
}
export default class AppRenameCmd extends FleetRenameCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet rename' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet rename'
`;
public static examples = [];
public static usage = 'app rename <fleet> [newName]';
public static args = FleetRenameCmd.args;
public static flags = FleetRenameCmd.flags;
public static authenticated = FleetRenameCmd.authenticated;
public static primary = FleetRenameCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -15,38 +15,44 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
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';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
application: string;
fleet: string;
}
export default class AppRestartCmd extends Command {
export class FleetRestartCmd extends Command {
public static description = stripIndent`
Restart an application.
Restart a fleet.
Restart all devices belonging to an application.
Restart all devices belonging to a fleet.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena app restart MyApp',
'$ balena app restart myorg/myapp',
'$ balena fleet restart MyFleet',
'$ balena fleet restart myorg/myfleet',
];
public static args = [ca.applicationRequired];
public static args = [ca.fleetRequired];
public static usage = 'app restart <application>';
public static usage = 'fleet restart <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
@ -54,16 +60,45 @@ export default class AppRestartCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRestartCmd);
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);
const application = await getApplication(balena, params.fleet);
await balena.models.application.restart(application.id);
}
}
export default class AppRestartCmd extends FleetRestartCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet restart' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet restart'
`;
public static examples = [];
public static usage = 'app restart <fleet>';
public static args = FleetRestartCmd.args;
public static flags = FleetRestartCmd.flags;
public static authenticated = FleetRestartCmd.authenticated;
public static primary = FleetRestartCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -15,12 +15,18 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
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';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
yes: boolean;
@ -28,14 +34,14 @@ interface FlagsDef {
}
interface ArgsDef {
application: string;
fleet: string;
}
export default class AppRmCmd extends Command {
export class FleetRmCmd extends Command {
public static description = stripIndent`
Remove an application.
Remove a fleet.
Permanently remove a balena application.
Permanently remove a fleet.
The --yes option may be used to avoid interactive confirmation.
@ -43,14 +49,14 @@ export default class AppRmCmd extends Command {
`;
public static examples = [
'$ balena app rm MyApp',
'$ balena app rm MyApp --yes',
'$ balena app rm myorg/myapp',
'$ balena fleet rm MyFleet',
'$ balena fleet rm MyFleet --yes',
'$ balena fleet rm myorg/myfleet',
];
public static args = [ca.applicationRequired];
public static args = [ca.fleetRequired];
public static usage = 'app rm <application>';
public static usage = 'fleet rm <fleet>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
@ -59,10 +65,9 @@ export default class AppRmCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
AppRmCmd,
);
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params, flags: options } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRmCmd);
const { confirm } = await import('../../utils/patterns');
const { getApplication } = await import('../../utils/sdk');
@ -71,13 +76,41 @@ export default class AppRmCmd extends Command {
// Confirm
await confirm(
options.yes ?? false,
`Are you sure you want to delete application ${params.application}?`,
`Are you sure you want to delete fleet ${params.fleet}?`,
);
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.application);
const application = await getApplication(balena, params.fleet);
// Remove
await balena.models.application.remove(application.id);
}
}
export default class AppRmCmd extends FleetRmCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet rm' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet rm'
`;
public static examples = [];
public static usage = 'app rm <fleet>';
public static args = FleetRmCmd.args;
public static flags = FleetRmCmd.flags;
public static authenticated = FleetRmCmd.authenticated;
public static primary = FleetRmCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRmCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,9 +16,12 @@
*/
import { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { appToFleetCmdMsg, warnify } from '../utils/messages';
interface ExtendedApplication extends ApplicationWithDeviceType {
device_count?: number;
@ -27,22 +30,22 @@ interface ExtendedApplication extends ApplicationWithDeviceType {
interface FlagsDef {
help: void;
verbose: boolean;
verbose?: boolean;
}
export default class AppsCmd extends Command {
export class FleetsCmd extends Command {
public static description = stripIndent`
List all applications.
List all fleets.
list all your balena applications.
List all your balena fleets.
For detailed information on a particular application,
use \`balena app <application>\` instead.
For detailed information on a particular fleet, use
\`balena fleet <fleet>\`
`;
public static examples = ['$ balena apps'];
public static examples = ['$ balena fleets'];
public static usage = 'apps';
public static usage = 'fleets';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
@ -56,8 +59,10 @@ export default class AppsCmd extends Command {
public static authenticated = true;
public static primary = true;
public async run() {
this.parse<FlagsDef, {}>(AppsCmd);
protected useAppWord = false;
public async run(_parserOutput?: ParserOutput<FlagsDef, {}>) {
_parserOutput ||= this.parse<FlagsDef, {}>(FleetsCmd);
const balena = getBalenaSdk();
@ -85,7 +90,7 @@ export default class AppsCmd extends Command {
console.log(
getVisuals().table.horizontal(applications, [
'id',
'app_name',
this.useAppWord ? 'app_name' : 'app_name => NAME',
'slug',
'device_type',
'online_devices',
@ -94,3 +99,36 @@ export default class AppsCmd extends Command {
);
}
}
const appsToFleetsRenameMsg = appToFleetCmdMsg
.replace(/'app'/g, "'apps'")
.replace(/'fleet'/g, "'fleets'");
export default class AppsCmd extends FleetsCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleets' command
${appsToFleetsRenameMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleets'
`;
public static examples = [];
public static usage = 'apps';
public static args = FleetsCmd.args;
public static flags = FleetsCmd.flags;
public static authenticated = FleetsCmd.authenticated;
public static primary = FleetsCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, {}>(AppsCmd);
if (process.stderr.isTTY) {
console.error(warnify(appsToFleetsRenameMsg));
}
this.useAppWord = true;
await super.run(parserOutput);
}
}

View File

@ -18,22 +18,27 @@
import { flags } from '@oclif/command';
import Command from '../command';
import { getBalenaSdk } from '../utils/lazy';
import * as cf from '../utils/common-flags';
import * as compose from '../utils/compose';
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
import {
appToFleetFlagMsg,
buildArgDeprecation,
dockerignoreHelp,
registrySecretsHelp,
warnify,
} from '../utils/messages';
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
import { buildProject, composeCliFlags } from '../utils/compose_ts';
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import { dockerCliFlags } from '../utils/docker';
import { isV13 } from '../utils/version';
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
arch?: string;
deviceType?: string;
application?: string;
fleet?: string;
source?: string; // Not part of command profile - source param copied here.
help: void;
}
@ -51,7 +56,7 @@ the provided docker daemon in your development machine or balena device.
(See also the \`balena push\` command for the option of building images in the
balenaCloud build servers.)
You must provide either an application or a device-type/architecture pair.
You must specify either a fleet, or the device type and architecture.
This command will look into the given source directory (or the current working
directory if one isn't specified) for a docker-compose.yml file, and if found,
@ -65,12 +70,12 @@ ${registrySecretsHelp}
${dockerignoreHelp}
`;
public static examples = [
'$ balena build --application myApp',
'$ balena build ./source/ --application myApp',
'$ balena build --fleet myFleet',
'$ balena build ./source/ --fleet myorg/myfleet',
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
'$ balena build --docker /var/run/docker.sock --application myApp # Linux, Mac',
'$ balena build --docker //./pipe/docker_engine --application myApp # Windows',
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -a myApp',
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
'$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows',
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
];
public static args = [
@ -91,10 +96,8 @@ ${dockerignoreHelp}
description: 'the type of device this build is for',
char: 'd',
}),
application: flags.string({
description: 'name of the target balena application this build is for',
char: 'a',
}),
...(isV13() ? {} : { application: cf.application }),
fleet: cf.fleet,
...composeCliFlags,
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
@ -109,10 +112,13 @@ ${dockerignoreHelp}
BuildCmd,
);
if (options.application && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
await Command.checkLoggedInIf(!!options.application);
// compositions with many services trigger misleading warnings
// @ts-ignore editing property that isn't typed but does exist
(await import('events')).defaultMaxListeners = 1000;
const sdk = getBalenaSdk();
@ -162,7 +168,7 @@ ${dockerignoreHelp}
) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
'You must specify either an application or an arch/deviceType pair to build for',
'You must specify either a fleet (-f), or the device type (-d) and architecture (-A)',
);
}
@ -233,7 +239,12 @@ ${dockerignoreHelp}
) {
const { loadProject } = await import('../utils/compose_ts');
const project = await loadProject(logger, composeOpts);
const project = await loadProject(
logger,
composeOpts,
undefined,
opts.buildOpts.t,
);
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
if (
@ -242,7 +253,7 @@ ${dockerignoreHelp}
!appType.supports_multicontainer
) {
logger.logWarn(
'Target application does not support multiple containers.\n' +
'Target fleet does not support multiple containers.\n' +
'Continuing with build, but you will not be able to deploy.',
);
}
@ -260,7 +271,7 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore,
nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore,
});
}

View File

@ -19,13 +19,19 @@ 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 {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import type { PineDeferred } from 'balena-sdk';
interface FlagsDef {
version: string; // OS version
application?: string;
app?: string; // application alias
fleet?: string;
device?: string;
deviceApiKey?: string;
deviceType?: string;
@ -43,16 +49,15 @@ export default class ConfigGenerateCmd extends Command {
public static description = stripIndent`
Generate a config.json file.
Generate a config.json file for a device or application.
Generate a config.json file for a device or fleet.
Calling this command with the exact version number of the targeted image is required.
The target balenaOS version must be specified with the --version option.
This command is interactive by default, but you can do this automatically without interactivity
by specifying an option for each question on the command line, if you know the questions
that will be asked for the relevant device type.
To configure an image for a fleet of mixed device types, use the --fleet option
alongside the --deviceType option to specify the target device type.
In case that you want to configure an image for an application with mixed device types,
you can pass the --deviceType argument along with --application to specify the target device type.
To avoid interactive questions, specify a command line option for each question that
would otherwise be asked.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
@ -62,11 +67,11 @@ export default class ConfigGenerateCmd extends Command {
'$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key',
'$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>',
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
'$ balena config generate --app MyApp --version 2.12.7',
'$ balena config generate --app 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',
'$ balena config generate --fleet MyFleet --version 2.12.7',
'$ balena config generate --fleet myorg/myfleet --version 2.12.7',
'$ balena config generate --fleet MyFleet --version 2.12.7 --deviceType fincm3',
'$ balena config generate --fleet MyFleet --version 2.12.7 --output config.json',
'$ balena config generate --fleet MyFleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
];
public static usage = 'config generate';
@ -76,20 +81,28 @@ export default class ConfigGenerateCmd extends Command {
description: 'a balenaOS version',
required: true,
}),
application: { ...cf.application, exclusive: ['app', 'device'] },
app: { ...cf.app, exclusive: ['application', 'device'] },
device: flags.string({
description: 'device uuid',
char: 'd',
exclusive: ['application', 'app'],
}),
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device'],
},
app: { ...cf.app, exclusive: ['application', 'fleet', 'device'] },
appUpdatePollInterval: flags.string({
description: 'DEPRECATED alias for --updatePollInterval',
}),
}),
fleet: { ...cf.fleet, exclusive: ['application', 'app', 'device'] },
device: { ...cf.device, exclusive: ['application', 'app', 'fleet'] },
deviceApiKey: flags.string({
description:
'custom device key - note that this is only supported on balenaOS 2.0.3+',
char: 'k',
}),
deviceType: flags.string({
description: 'device type slug',
description:
"device type slug (run 'balena devices supported' for possible values)",
}),
'generate-device-api-key': flags.boolean({
description: 'generate a fresh device key for the device',
@ -113,7 +126,7 @@ export default class ConfigGenerateCmd extends Command {
}),
appUpdatePollInterval: flags.string({
description:
'how frequently (in minutes) to poll for application updates',
'supervisor cloud polling interval in minutes (e.g. for variable updates)',
}),
help: cf.help,
};
@ -143,8 +156,8 @@ export default class ConfigGenerateCmd extends Command {
if (!rawDevice.belongs_to__application) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(stripIndent`
Device ${options.device} does not appear to belong to an accessible application.
Try with a different device, or use '--application' instead of '--device'.`);
Device ${options.device} does not appear to belong to an accessible fleet.
Try with a different device, or use '--fleet' instead of '--device'.`);
}
device = rawDevice as DeviceWithDeviceType & {
belongs_to__application: PineDeferred;
@ -177,7 +190,7 @@ export default class ConfigGenerateCmd extends Command {
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
) {
throw new balena.errors.BalenaInvalidDeviceType(
`Device type ${options.deviceType} is incompatible with application ${options.application}`,
`Device type ${options.deviceType} is incompatible with fleet ${options.application}`,
);
}
}
@ -218,7 +231,7 @@ export default class ConfigGenerateCmd extends Command {
}
protected readonly missingDeviceOrAppMessage = stripIndent`
Either a device or an application must be specified.
Either a device or a fleet must be specified.
See the help page for examples:
@ -226,13 +239,16 @@ export default class ConfigGenerateCmd extends Command {
`;
protected readonly deviceTypeNotAllowedMessage =
'The --deviceType option can only be used alongside the --application option';
'The --deviceType option can only be used alongside the --fleet option';
protected async validateOptions(options: FlagsDef) {
const { ExpectedError } = await import('../../errors');
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
if (options.device == null && options.application == null) {

View File

@ -34,8 +34,11 @@ export default class ConfigInjectCmd extends Command {
public static description = stripIndent`
Inject a configuration file into a device or OS image.
Inject a config.json file to the mounted filesystem,
e.g. the SD card of a provisioned device or balenaOS image.
Inject a config.json file to a mounted filesystem, e.g. the SD card of a
provisioned device or balenaOS image.
Note: if using a private/custom device type, please ensure you are logged in
('balena login' command). Public device types do not require logging in.
`;
public static examples = [
@ -59,8 +62,6 @@ export default class ConfigInjectCmd extends Command {
help: cf.help,
};
public static authenticated = true;
public static root = true;
public async run() {
@ -68,7 +69,7 @@ export default class ConfigInjectCmd extends Command {
ConfigInjectCmd,
);
const { safeUmount } = await import('../../utils/helpers');
const { safeUmount } = await import('../../utils/umount');
const drive =
options.drive || (await getVisuals().drive('Select the device/OS drive'));

View File

@ -54,7 +54,7 @@ export default class ConfigReadCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
const { safeUmount } = await import('../../utils/helpers');
const { safeUmount } = await import('../../utils/umount');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));

View File

@ -58,7 +58,7 @@ export default class ConfigReconfigureCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
const { safeUmount } = await import('../../utils/helpers');
const { safeUmount } = await import('../../utils/umount');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));

View File

@ -75,7 +75,7 @@ export default class ConfigWriteCmd extends Command {
ConfigWriteCmd,
);
const { safeUmount } = await import('../../utils/helpers');
const { denyMount, safeUmount } = await import('../../utils/umount');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));
@ -85,13 +85,23 @@ export default class ConfigWriteCmd extends Command {
const configJSON = await config.read(drive, options.type);
console.info(`Setting ${params.key} to ${params.value}`);
const _ = await import('lodash');
_.set(configJSON, params.key, params.value);
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
await safeUmount(drive);
await config.write(drive, options.type, configJSON);
await denyMount(drive, async () => {
await safeUmount(drive);
await config.write(drive, options.type, configJSON);
});
console.info('Done');
}
/** Call Lodash's _.setWith(). Moved here for ease of testing. */
static updateConfigJson(configJSON: object, key: string, value: string) {
const _ = require('lodash') as typeof import('lodash');
// note: _.setWith() is needed instead of _.set() because, given a key
// like `os.udevRules.101`, _.set() creates a udevRules array (rather
// than a dictionary) and sets the 101st array element to value, while
// we actually want udevRules to be dictionary like { '101': value }
_.setWith(configJSON, key, value, (v) => v || {});
}
}

View File

@ -26,6 +26,7 @@ import {
registrySecretsHelp,
buildArgDeprecation,
} from '../utils/messages';
import * as ca from '../utils/common-args';
import * as compose from '../utils/compose';
import type {
BuiltImage,
@ -33,7 +34,7 @@ import type {
ComposeOpts,
Release as ComposeReleaseInfo,
} from '../utils/compose-types';
import type { DockerCliFlags } from '../utils/docker';
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import {
applyReleaseTagKeysAndValues,
buildProject,
@ -58,22 +59,23 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
build: boolean;
nologupload: boolean;
'release-tag'?: string[];
draft: boolean;
help: void;
}
interface ArgsDef {
appName: string;
fleet: string;
image?: string;
}
export default class DeployCmd extends Command {
public static description = `\
Deploy a single image or a multicontainer project to a balena application.
Deploy a single image or a multicontainer project to a balena fleet.
Usage: \`deploy <appName> ([image] | --build [--source build-dir])\`
Usage: \`deploy <fleet> ([image] | --build [--source build-dir])\`
Use this command to deploy an image or a complete multicontainer project to an
application, optionally building it first. The source images are searched for
Use this command to deploy an image or a complete multicontainer project to a
fleet, optionally building it first. The source images are searched for
(and optionally built) using the docker daemon in your development machine or
balena device. (See also the \`balena push\` command for the option of building
the image in the balenaCloud build servers.)
@ -88,8 +90,8 @@ If a compose file isn't found, the command will look for a Dockerfile[.template]
file (or alternative Dockerfile specified with the \`-f\` option), and if yet
that isn't found, it will try to generate one.
To deploy to an app on which you're a collaborator, use
\`balena deploy <appOwnerUsername>/<appName>\`.
To deploy to a fleet where you are a collaborator, use fleet slug including the
organization: \`balena deploy <organization>/<fleet>\`.
${registrySecretsHelp}
@ -97,25 +99,21 @@ ${dockerignoreHelp}
`;
public static examples = [
'$ 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"',
'$ balena deploy myFleet',
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
'$ balena deploy myorg/myfleet myRepo/myImage',
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
];
public static args = [
{
name: 'appName',
description: 'the name of the application to deploy to',
required: true,
},
ca.fleetRequired,
{
name: 'image',
description: 'the image to deploy',
},
];
public static usage = 'deploy <appName> [image]';
public static usage = 'deploy <fleet> [image]';
public static flags: flags.Input<FlagsDef> = {
source: flags.string({
@ -139,6 +137,14 @@ ${dockerignoreHelp}
`,
multiple: true,
}),
draft: flags.boolean({
description: stripIndent`
Deploy the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.`,
default: false,
}),
...composeCliFlags,
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
@ -155,14 +161,12 @@ ${dockerignoreHelp}
DeployCmd,
);
// compositions with many services trigger misleading warnings
// @ts-ignore editing property that isn't typed but does exist
(await import('events')).defaultMaxListeners = 1000;
const logger = await Command.getLogger();
logger.logDebug('Parsing input...');
const { appName, image } = params;
const { fleet, image } = params;
// Build args are under consideration for removal - warn user
if (options.buildArg) {
@ -190,21 +194,19 @@ ${dockerignoreHelp}
options['registry-secrets'],
);
} else {
const {
dockerfilePath,
registrySecrets,
} = await validateProjectDirectory(sdk, {
dockerfilePath: options.dockerfile,
noParentCheck: options['noparent-check'] || false,
projectPath: options.source || '.',
registrySecretsPath: options['registry-secrets'],
});
const { dockerfilePath, registrySecrets } =
await validateProjectDirectory(sdk, {
dockerfilePath: options.dockerfile,
noParentCheck: options['noparent-check'] || false,
projectPath: options.source || '.',
registrySecretsPath: options['registry-secrets'],
});
options.dockerfile = dockerfilePath;
options['registry-secrets'] = registrySecrets;
}
const helpers = await import('../utils/helpers');
const app = await helpers.getAppWithArch(appName);
const app = await helpers.getAppWithArch(fleet);
const dockerUtils = await import('../utils/docker');
const [docker, buildOpts, composeOpts] = await Promise.all([
@ -215,11 +217,12 @@ ${dockerignoreHelp}
const release = await this.deployProject(docker, logger, composeOpts, {
app,
appName, // may be prefixed by 'owner/', unlike app.app_name
appName: fleet, // may be prefixed by 'owner/', unlike app.app_name
image,
shouldPerformBuild: !!options.build,
shouldUploadLogs: !options.nologupload,
buildEmulated: !!options.emulated,
createAsDraft: options.draft,
buildOpts,
});
await applyReleaseTagKeysAndValues(
@ -242,7 +245,8 @@ ${dockerignoreHelp}
shouldPerformBuild: boolean;
shouldUploadLogs: boolean;
buildEmulated: boolean;
buildOpts: any; // arguments to forward to docker build command
buildOpts: BuildOpts;
createAsDraft: boolean;
},
) {
const _ = await import('lodash');
@ -255,10 +259,15 @@ ${dockerignoreHelp}
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
try {
const project = await loadProject(logger, composeOpts, opts.image);
const project = await loadProject(
logger,
composeOpts,
opts.image,
opts.buildOpts.t,
);
if (project.descriptors.length > 1 && !appType?.supports_multicontainer) {
throw new ExpectedError(
'Target application does not support multiple containers. Aborting!',
'Target fleet does not support multiple containers. Aborting!',
);
}
@ -310,7 +319,7 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore,
nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore,
});
builtImagesByService = _.keyBy(builtImages, 'serviceName');
@ -330,7 +339,7 @@ ${dockerignoreHelp}
const { deployLegacy } = require('../utils/deploy-legacy');
const msg = getChalk().yellow(
'Target application requires legacy deploy method.',
'Target fleet requires legacy deploy method.',
);
logger.logWarn(msg);
@ -374,6 +383,8 @@ ${dockerignoreHelp}
`Bearer ${auth}`,
apiEndpoint,
!opts.shouldUploadLogs,
composeOpts.projectPath,
opts.createAsDraft,
);
}

View File

@ -21,7 +21,10 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { appToFleetOutputMsg, warnify } from '../../utils/messages';
import { tryAsInteger } from '../../utils/validation';
import { isV13 } from '../../utils/version';
import type { Application, Release } from 'balena-sdk';
interface ExtendedDevice extends DeviceWithDeviceType {
@ -43,6 +46,7 @@ interface ExtendedDevice extends DeviceWithDeviceType {
interface FlagsDef {
help: void;
v13: boolean;
}
interface ArgsDef {
@ -70,13 +74,17 @@ export default class DeviceCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
v13: cf.v13,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceCmd,
);
const useAppWord = !options.v13 && !isV13();
const balena = getBalenaSdk();
@ -111,7 +119,8 @@ export default class DeviceCmd extends Command {
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication = device.belongs_to__application as Application[];
const belongsToApplication =
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
@ -161,6 +170,10 @@ export default class DeviceCmd extends Command {
);
}
if (useAppWord && process.stderr.isTTY) {
console.error(warnify(appToFleetOutputMsg));
}
console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
@ -171,7 +184,7 @@ export default class DeviceCmd extends Command {
'ip_address',
'public_address',
'mac_address',
'application_name',
useAppWord ? 'application_name' : 'application_name => FLEET',
'last_seen',
'uuid',
'commit',

View File

@ -19,12 +19,18 @@ 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 {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { runCommand } from '../../utils/helpers';
import { isV13 } from '../../utils/version';
interface FlagsDef {
application?: string;
app?: string;
fleet?: string;
yes: boolean;
advanced: boolean;
'os-version'?: string;
@ -37,26 +43,30 @@ export default class DeviceInitCmd extends Command {
public static description = stripIndent`
Initialize a device with balenaOS.
Initialize a device by downloading the OS image of a certain application
Initialize a device by downloading the OS image of the specified fleet
and writing it to an SD Card.
Note, if the application option is omitted it will be prompted
for interactively.
If the --fleet 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',
'$ balena device init --fleet MyFleet',
'$ balena device init -f myorg/myfleet',
];
public static usage = 'device init';
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
app: cf.app,
...(isV13()
? {}
: {
application: cf.application,
app: cf.app,
}),
fleet: cf.fleet,
yes: cf.yes,
advanced: flags.boolean({
char: 'v',
@ -95,15 +105,20 @@ export default class DeviceInitCmd extends Command {
const logger = await Command.getLogger();
const balena = getBalenaSdk();
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
// Consolidate application options
options.application = options.application || options.app;
options.application ||= options.app || options.fleet;
delete options.app;
// Get application and
const application = (await getApplication(
balena,
options['application'] ||
(await (await import('../../utils/patterns')).selectApplication()).id,
(
await (await import('../../utils/patterns')).selectApplication()
).id,
{
$expand: {
is_for__device_type: {

View File

@ -15,22 +15,36 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import type { Application, BalenaSDK } from 'balena-sdk';
import type {
BalenaSDK,
Device,
DeviceType,
PineTypedResult,
} from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
interface ExtendedDevice extends DeviceWithDeviceType {
type ExtendedDevice = PineTypedResult<
Device,
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
> & {
application_name?: string;
}
};
interface FlagsDef {
application?: string;
app?: string;
fleet?: string;
help: void;
}
@ -40,12 +54,11 @@ interface ArgsDef {
export default class DeviceMoveCmd extends Command {
public static description = stripIndent`
Move one or more devices to another application.
Move one or more devices to another fleet.
Move one or more devices to another application.
Move one or more devices to another fleet.
Note, if the application option is omitted it will be prompted
for interactively.
If --fleet is omitted, the fleet will be prompted for interactively.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
@ -53,8 +66,8 @@ export default class DeviceMoveCmd extends Command {
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',
'$ balena device move 7cf02a6 --fleet MyNewFleet',
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
];
public static args: Array<IArg<any>> = [
@ -69,8 +82,8 @@ export default class DeviceMoveCmd extends Command {
public static usage = 'device move <uuid(s)>';
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
app: cf.app,
...(isV13() ? {} : { app: cf.app, application: cf.application }),
fleet: cf.fleet,
help: cf.help,
};
@ -81,13 +94,15 @@ export default class DeviceMoveCmd extends Command {
DeviceMoveCmd,
);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
const balena = getBalenaSdk();
const { tryAsInteger } = await import('../../utils/validation');
const { expandForAppName } = await import('../../utils/helpers');
options.application = options.application || options.app;
delete options.app;
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
// Parse ids string into array of correct types
const deviceIds: Array<string | number> = params.uuid
@ -98,15 +113,16 @@ export default class DeviceMoveCmd extends Command {
const devices = await Promise.all(
deviceIds.map(
(uuid) =>
balena.models.device.get(uuid, expandForAppName) as Promise<
ExtendedDevice
>,
balena.models.device.get(
uuid,
expandForAppNameAndCpuArch,
) as Promise<ExtendedDevice>,
),
);
// Map application name for each device
for (const device of devices) {
const belongsToApplication = device.belongs_to__application as Application[];
const belongsToApplication = device.belongs_to__application;
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
@ -124,9 +140,7 @@ export default class DeviceMoveCmd extends Command {
for (const uuid of deviceIds) {
try {
await balena.models.device.move(uuid, application.id);
console.info(
`Device ${uuid} was moved to application ${application.slug}`,
);
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
} catch (err) {
console.info(`${err.message}, uuid: ${uuid}`);
process.exitCode = 1;
@ -138,43 +152,56 @@ export default class DeviceMoveCmd extends Command {
balena: BalenaSDK,
devices: ExtendedDevice[],
) {
const [deviceDeviceTypes, deviceTypes] = await Promise.all([
Promise.all(
devices.map((device) =>
balena.models.device.getManifestBySlug(
device.is_of__device_type[0].slug,
),
const { getExpandedProp } = await import('../../utils/pine');
// deduplicate the slugs
const deviceCpuArchs = Array.from(
new Set(
devices.map(
(d) => d.is_of__device_type[0].is_of__cpu_architecture[0].slug,
),
),
balena.models.config.getDeviceTypes(),
]);
);
const compatibleDeviceTypes = deviceTypes.filter((dt) =>
deviceDeviceTypes.every(
(deviceDeviceType) =>
balena.models.os.isArchitectureCompatibleWith(
deviceDeviceType.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceDeviceType.isDependent &&
dt.state !== 'DISCONTINUED',
),
const deviceTypeOptions = {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
} as const;
const deviceTypes = (await balena.models.deviceType.getAllSupported(
deviceTypeOptions,
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
const compatibleDeviceTypeSlugs = new Set(
deviceTypes
.filter((deviceType) => {
const deviceTypeArch = getExpandedProp(
deviceType.is_of__cpu_architecture,
'slug',
)!;
return deviceCpuArchs.every((deviceCpuArch) =>
balena.models.os.isArchitectureCompatibleWith(
deviceCpuArch,
deviceTypeArch,
),
);
})
.map((deviceType) => deviceType.slug),
);
const patterns = await import('../../utils/patterns');
try {
const application = await patterns.selectApplication(
(app) =>
compatibleDeviceTypes.some(
(dt) => dt.slug === app.is_for__device_type[0].slug,
) &&
// @ts-ignore using the extended device object prop
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
devices.some((device) => device.application_name !== app.app_name),
true,
);
return application;
} catch (err) {
if (deviceDeviceTypes.length) {
if (!compatibleDeviceTypeSlugs.size) {
throw new ExpectedError(
`${err.message}\nDo all devices have a compatible architecture?`,
);

View File

@ -79,19 +79,15 @@ export default class DeviceOsUpdateCmd extends Command {
const sdk = getBalenaSdk();
// Get device info
const {
uuid,
is_of__device_type,
os_version,
os_variant,
} = (await sdk.models.device.get(params.uuid, {
$select: ['uuid', 'os_version', 'os_variant'],
$expand: {
is_of__device_type: {
$select: 'slug',
const { uuid, is_of__device_type, os_version, os_variant } =
(await sdk.models.device.get(params.uuid, {
$select: ['uuid', 'os_version', 'os_variant'],
$expand: {
is_of__device_type: {
$select: 'slug',
},
},
},
})) as DeviceWithDeviceType;
})) as DeviceWithDeviceType;
// Get current device OS version
const currentOsVersion = sdk.models.device.getOsVersion({

View File

@ -31,10 +31,10 @@ interface ArgsDef {
export default class DevicePurgeCmd extends Command {
public static description = stripIndent`
Purge application data from a device.
Purge data from a device.
Purge application data from a device.
This will clear the application's /data directory.
Purge data from a device.
This will clear the device's "/data" directory.
Multiple devices may be specified with a comma-separated list
of values (no spaces).

View File

@ -29,27 +29,29 @@ interface FlagsDef {
}
interface ArgsDef {
application: string;
fleet: string;
}
export default class DeviceRegisterCmd extends Command {
public static description = stripIndent`
Register a device.
Register a new device.
Register a device to an application.
Register a new device with a balena fleet.
If --uuid is not provided, a new UUID will be automatically assigned.
${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>',
'$ balena device register MyFleet',
'$ balena device register MyFleet --uuid <uuid>',
'$ balena device register myorg/myfleet --uuid <uuid>',
];
public static args: Array<IArg<any>> = [ca.applicationRequired];
public static args: Array<IArg<any>> = [ca.fleetRequired];
public static usage = 'device register <application>';
public static usage = 'device register <fleet>';
public static flags: flags.Input<FlagsDef> = {
uuid: flags.string({
@ -70,7 +72,7 @@ export default class DeviceRegisterCmd extends Command {
const balena = getBalenaSdk();
const application = await getApplication(balena, params.application);
const application = await getApplication(balena, params.fleet);
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.app_name}: ${uuid}`);

View File

@ -20,7 +20,15 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
import {
applicationIdInfo,
appToFleetFlagMsg,
appToFleetOutputMsg,
jsonInfo,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import type { Application } from 'balena-sdk';
interface ExtendedDevice extends DeviceWithDeviceType {
@ -32,17 +40,19 @@ interface ExtendedDevice extends DeviceWithDeviceType {
interface FlagsDef {
application?: string;
app?: string;
fleet?: string;
help: void;
json: boolean;
v13: boolean;
}
export default class DevicesCmd extends Command {
public static description = stripIndent`
List all devices.
list all devices that belong to you.
List all of your devices.
You can filter the devices by application by using the \`--application\` option.
Devices can be filtered by fleet with the \`--fleet\` option.
${applicationIdInfo.split('\n').join('\n\t\t')}
@ -50,33 +60,51 @@ export default class DevicesCmd extends Command {
`;
public static examples = [
'$ balena devices',
'$ balena devices --application MyApp',
'$ balena devices --app MyApp',
'$ balena devices -a MyApp',
'$ balena devices -a myorg/myapp',
'$ balena devices --fleet MyFleet',
'$ balena devices -f myorg/myfleet',
];
public static usage = 'devices';
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
app: cf.app,
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'v13'],
},
app: { ...cf.app, exclusive: ['application', 'fleet', 'v13'] },
}),
fleet: { ...cf.fleet, exclusive: ['app', 'application'] },
json: cf.json,
help: cf.help,
v13: cf.v13,
};
public static primary = true;
public static authenticated = true;
protected useAppWord = false;
protected hasWarned = false;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
this.useAppWord = !options.fleet && !options.v13 && !isV13();
const balena = getBalenaSdk();
if (
(options.application || options.app) &&
!options.json &&
process.stderr.isTTY
) {
this.hasWarned = true;
console.error(warnify(appToFleetFlagMsg));
}
// Consolidate application options
options.application = options.application || options.app;
delete options.app;
options.application ||= options.app || options.fleet;
let devices;
@ -96,7 +124,8 @@ export default class DevicesCmd extends Command {
devices = devices.map(function (device) {
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication = device.belongs_to__application as Application[];
const belongsToApplication =
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]?.app_name || null;
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
@ -105,28 +134,32 @@ export default class DevicesCmd extends Command {
return device;
});
const jName = this.useAppWord ? 'application_name' : 'fleet_name';
const tName = this.useAppWord ? 'APPLICATION NAME' : 'FLEET';
const fields = [
'id',
'uuid',
'device_name',
'device_type',
'application_name',
options.json
? `application_name => ${jName}`
: `application_name => ${tName}`,
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
const _ = await import('lodash');
if (options.json) {
console.log(
JSON.stringify(
devices.map((device) => _.pick(device, fields)),
null,
4,
),
);
const { pickAndRename } = await import('../../utils/helpers');
const mapped = devices.map((device) => pickAndRename(device, fields));
console.log(JSON.stringify(mapped, null, 4));
} else {
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
console.error(warnify(appToFleetOutputMsg));
}
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),

View File

@ -21,10 +21,16 @@ 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';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string; // device UUID
help: void;
quiet: boolean;
@ -38,18 +44,17 @@ interface ArgsDef {
export default class EnvAddCmd extends Command {
public static description = stripIndent`
Add env or config variable to application(s), device(s) or service(s).
Add env or config variable to fleets, devices or services.
Add an environment or config variable to one or more applications, devices
or services, as selected by the respective command-line options. Either the
--application or the --device option must be provided, and either may be be
Add an environment or config variable to one or more fleets, devices or
services, as selected by the respective command-line options. Either the
--fleet or the --device option must be provided, and either may be be
used alongside the --service option to define a service-specific variable.
(A service is an application container in a "microservices" application.)
(A service corresponds to a Docker image/container in a microservices fleet.)
When the --service option is used in conjunction with the --device option,
the service variable applies to the selected device only. Otherwise, it
applies to all devices of the selected application (i.e., the application's
fleet). If the --service option is omitted, the variable applies to all
services.
the service variable applies to the selected device only. Otherwise, it
applies to all devices of the selected fleet. If the --service option is
omitted, the variable applies to all services.
If VALUE is omitted, the CLI will attempt to use the value of the environment
variable of same name in the CLI process' environment. In this case, a warning
@ -61,19 +66,18 @@ export default class EnvAddCmd extends Command {
running on devices. They are also stored differently in the balenaCloud API
database. Configuration variables cannot be set for specific services,
therefore the --service option cannot be used when the variable name starts
with a reserved prefix. When defining custom application variables, please
avoid the reserved prefixes.
with a reserved prefix. When defining custom fleet variables, please avoid
these 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',
'$ balena env add TERM --fleet MyFleet',
'$ balena env add EDITOR vim -f myorg/myfleet',
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2',
'$ balena env add EDITOR vim --fleet MyFleet --service MyService',
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2 --service MyService,MyService2',
'$ balena env add EDITOR vim --device 7cf02a6',
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433',
'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
@ -97,8 +101,11 @@ export default class EnvAddCmd extends Command {
public static usage = 'env add <name> [value]';
public static flags: flags.Input<FlagsDef> = {
application: { ...cf.application, exclusive: ['device'] },
device: { ...cf.device, exclusive: ['application'] },
...(isV13()
? {}
: { application: { ...cf.application, exclusive: ['fleet', 'device'] } }),
fleet: { ...cf.fleet, exclusive: ['application', 'device'] },
device: { ...cf.device, exclusive: ['application', 'fleet'] },
help: cf.help,
quiet: cf.quiet,
service: cf.service,
@ -110,9 +117,13 @@ export default class EnvAddCmd extends Command {
);
const cmd = this;
if (options.application && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
if (!options.application && !options.device) {
throw new ExpectedError(
'Either the --application or the --device option must be specified',
'Either the --fleet or the --device option must be specified',
);
}
@ -161,7 +172,7 @@ export default class EnvAddCmd extends Command {
params.value,
);
} catch (err) {
console.error(`${err.message}, app: ${app}`);
console.error(`${err.message}, fleet: ${app}`);
process.exitCode = 1;
}
}
@ -183,7 +194,7 @@ export default class EnvAddCmd extends Command {
}
/**
* Add service variables for a device or application.
* Add service variables for a device or fleet.
*/
async function setServiceVars(
sdk: BalenaSdk.BalenaSDK,
@ -201,7 +212,7 @@ async function setServiceVars(
params.value!,
);
} catch (err) {
console.error(`${err.message}, application: ${app}`);
console.error(`${err.message}, fleet: ${app}`);
process.exitCode = 1;
}
}
@ -216,7 +227,7 @@ async function setServiceVars(
sdk,
uuid,
['id'],
['app_name'],
['slug'],
);
} catch (err) {
console.error(`${err.message}, device: ${uuid}`);
@ -225,11 +236,7 @@ async function setServiceVars(
}
for (const service of options.service!.split(',')) {
try {
const serviceId = await getServiceIdForApp(
sdk,
app.app_name,
service,
);
const serviceId = await getServiceIdForApp(sdk, app.slug, service);
await sdk.models.device.serviceVar.set(
device.id,
serviceId,
@ -262,7 +269,7 @@ async function getServiceIdForApp(
}
if (serviceId === undefined) {
throw new ExpectedError(
`Cannot find service ${serviceName} for application ${appName}`,
`Cannot find service ${serviceName} for fleet ${appName}`,
);
}
return serviceId;

View File

@ -38,9 +38,9 @@ interface ArgsDef {
export default class EnvRenameCmd extends Command {
public static description = stripIndent`
Change the value of a config or env var for an app, device or service.
Change the value of a config or env var for a fleet, device or service.
Change the value of a configuration or environment variable for an application,
Change the value of a configuration or environment variable for a fleet,
device or service, as selected by command-line options.
${ec.rmRenameHelp.split('\n').join('\n\t\t')}

View File

@ -37,9 +37,9 @@ interface ArgsDef {
export default class EnvRmCmd extends Command {
public static description = stripIndent`
Remove a config or env var from an application, device or service.
Remove a config or env var from a fleet, device or service.
Remove a configuration or environment variable from an application, device
Remove a configuration or environment variable from a fleet, device
or service, as selected by command-line options.
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
@ -91,8 +91,6 @@ export default class EnvRmCmd extends Command {
await confirm(
opt.yes || false,
'Are you sure you want to delete the environment variable?',
undefined,
true,
);
const balena = getBalenaSdk();

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2019 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,17 +21,24 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import {
applicationIdInfo,
appToFleetFlagMsg,
appToFleetOutputMsg,
warnify,
} from '../utils/messages';
import { isV13 } from '../utils/version';
interface FlagsDef {
application?: string;
fleet?: string;
config: boolean;
device?: string; // device UUID
json: boolean;
help: void;
service?: string; // service name
verbose: boolean;
v13: boolean;
}
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
@ -56,20 +63,20 @@ interface ServiceEnvironmentVariableInfo
export default class EnvsCmd extends Command {
public static description = stripIndent`
List the environment or config variables of an application, device or service.
List the environment or config variables of a fleet, 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.)
List the environment or configuration variables of a fleet, device or
service, as selected by the respective command-line options. (A service
corresponds to a Docker image/container in a microservices fleet.)
The results include application-wide (fleet), device-wide (multiple services on
a device) and service-specific variables that apply to the selected application,
device or service. It can be thought of as including "inherited" variables;
for example, a service inherits device-wide variables, and a device inherits
application-wide variables.
The results include fleet-wide (multiple devices), device-specific (multiple
services on a specific device) and service-specific variables that apply to the
selected fleet, device or service. It can be thought of as including inherited
variables; for example, a service inherits device-wide variables, and a device
inherits fleet-wide variables.
The printed output may include DEVICE and/or SERVICE columns to distinguish
between application-wide, device-specific and service-specific variables.
between fleet-wide, device-specific and service-specific variables.
An asterisk in these columns indicates that the variable applies to
"all devices" or "all services".
@ -83,22 +90,22 @@ export default class EnvsCmd extends Command {
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).
for the given query. When querying variables for a device, note that the fleet
name may be null in JSON output (or 'N/A' in tabular output) if the fleet that
the device belonged to is no longer accessible by the current user (for example,
in case the current user was removed from the fleet by the fleet's owner).
${applicationIdInfo.split('\n').join('\n\t\t')}
${appToFleetOutputMsg.split('\n').join('\n\t\t')}
`;
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 --fleet myorg/myfleet',
'$ balena envs --fleet MyFleet --json',
'$ balena envs --fleet MyFleet --service MyService',
'$ balena envs --fleet MyFleet --service MyService',
'$ balena envs --fleet MyFleet --config',
'$ balena envs --device 7cf02a6',
'$ balena envs --device 7cf02a6 --json',
'$ balena envs --device 7cf02a6 --config --json',
@ -113,34 +120,47 @@ export default class EnvsCmd extends Command {
: {
all: flags.boolean({
default: false,
description: stripIndent`
No-op since balena CLI v12.0.0.`,
description: 'No-op since balena CLI v12.0.0.',
hidden: true,
}),
application: {
exclusive: ['device', 'fleet', 'v13'],
...cf.application,
},
}),
application: { exclusive: ['device'], ...cf.application },
fleet: { exclusive: ['device', 'application'], ...cf.fleet },
config: flags.boolean({
default: false,
char: 'c',
description: 'show configuration variables only',
exclusive: ['service'],
}),
device: { exclusive: ['application'], ...cf.device },
device: { exclusive: ['fleet', 'application'], ...cf.device },
help: cf.help,
json: cf.json,
verbose: cf.verbose,
service: { exclusive: ['config'], ...cf.service },
v13: cf.v13,
};
protected useAppWord = false;
protected hasWarned = false;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
this.useAppWord = !options.fleet && !options.v13 && !isV13();
const variables: EnvironmentVariableInfo[] = [];
await Command.checkLoggedIn();
if (options.application && !options.json && process.stderr.isTTY) {
this.hasWarned = true;
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
if (!options.application && !options.device) {
throw new ExpectedError('You must specify an application or device');
throw new ExpectedError('Missing --fleet or --device option');
}
const balena = getBalenaSdk();
@ -154,11 +174,11 @@ export default class EnvsCmd extends Command {
balena,
options.device,
['uuid'],
['app_name'],
['slug'],
);
fullUUID = device.uuid;
if (app) {
appNameOrSlug = app.app_name;
appNameOrSlug = app.slug;
}
}
if (appNameOrSlug && options.service) {
@ -174,7 +194,7 @@ export default class EnvsCmd extends Command {
const target =
(options.service ? `service "${options.service}" of ` : '') +
(options.application
? `application "${options.application}"`
? `fleet "${options.application}"`
: `device "${options.device}"`);
throw new ExpectedError(`No environment variables found for ${target}`);
}
@ -190,11 +210,20 @@ export default class EnvsCmd extends Command {
// Replace undefined app names with 'N/A' or null
varArray = varArray.map((i: EnvironmentVariableInfo) => {
i.appName = i.appName || (options.json ? null : 'N/A');
if (i.appName) {
// use slug in v13, app name in v12 for compatibility
i.appName = isV13()
? i.appName
: i.appName.substring(i.appName.indexOf('/') + 1);
} else {
i.appName = options.json ? null : 'N/A';
}
return i;
});
fields.push(options.json ? 'appName' : 'appName => APPLICATION');
const jName = this.useAppWord ? 'appName' : 'fleetName';
const tName = this.useAppWord ? 'APPLICATION' : 'FLEET';
fields.push(options.json ? `appName => ${jName}` : `appName => ${tName}`);
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
@ -203,10 +232,13 @@ export default class EnvsCmd extends Command {
}
if (options.json) {
this.log(
stringifyVarArray<SDK.EnvironmentVariableBase>(varArray, fields),
);
const { pickAndRename } = await import('../utils/helpers');
const mapped = varArray.map((o) => pickAndRename(o, fields));
this.log(JSON.stringify(mapped, null, 4));
} else {
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
console.error(warnify(appToFleetOutputMsg));
}
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
@ -227,7 +259,7 @@ async function validateServiceName(
});
if (services.length === 0) {
throw new ExpectedError(
`Service "${serviceName}" not found for application "${appName}"`,
`Service "${serviceName}" not found for fleet "${appName}"`,
);
}
}
@ -344,33 +376,13 @@ function fillInInfoFields(
envVar.serviceName = (envVar.service as SDK.Service[])[0]?.service_name;
} else if ('service_install' in envVar) {
// envVar is of type DeviceServiceEnvironmentVariableInfo
envVar.serviceName = ((envVar.service_install as SDK.ServiceInstall[])[0]
?.installs__service as SDK.Service[])[0]?.service_name;
envVar.serviceName = (
(envVar.service_install as SDK.ServiceInstall[])[0]
?.installs__service as SDK.Service[]
)[0]?.service_name;
}
envVar.appName = appNameOrSlug;
envVar.serviceName = envVar.serviceName || '*';
envVar.deviceUUID = deviceUUID || '*';
}
}
/**
* Transform each object (item) of varArray to preserve only the
* fields (keys) listed in the fields argument.
*/
function stringifyVarArray<T = Dictionary<any>>(
varArray: T[],
fields: string[],
): string {
const transformed = varArray.map((o: Dictionary<any>) =>
_.transform(
o,
(result, value, key) => {
if (fields.includes(key)) {
result[key] = value;
}
},
{} as Dictionary<any>,
),
);
return JSON.stringify(transformed, null, 4);
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2020 Balena Ltd.
* 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.
@ -15,13 +15,6 @@
* limitations under the License.
*/
declare module 'umount' {
export const umount: (
device: string,
callback: (err?: Error, stdout?: any, stderr?: any) => void,
) => void;
export const isMounted: (
device: string,
callback: (err: Error | null, isMounted?: boolean) => void,
) => void;
}
import { FleetCreateCmd } from '../app/create';
export default FleetCreateCmd;

View File

@ -0,0 +1,20 @@
/**
* @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 { FleetCmd } from '../app';
export default FleetCmd;

View File

@ -0,0 +1,20 @@
/**
* @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 { FleetPurgeCmd } from '../app/purge';
export default FleetPurgeCmd;

View File

@ -0,0 +1,20 @@
/**
* @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 { FleetRenameCmd } from '../app/rename';
export default FleetRenameCmd;

View File

@ -0,0 +1,20 @@
/**
* @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 { FleetRestartCmd } from '../app/restart';
export default FleetRestartCmd;

20
lib/commands/fleet/rm.ts Normal file
View File

@ -0,0 +1,20 @@
/**
* @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 { FleetRmCmd } from '../app/rm';
export default FleetRmCmd;

20
lib/commands/fleets.ts Normal file
View File

@ -0,0 +1,20 @@
/**
* @license
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 { FleetsCmd } from './apps';
export default FleetsCmd;

View File

@ -21,9 +21,11 @@ import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import { parseAsLocalHostnameOrIp } from '../utils/validation';
import { isV13 } from '../utils/version';
interface FlagsDef {
application?: string;
fleet?: string;
pollInterval?: number;
help?: void;
}
@ -34,22 +36,22 @@ interface ArgsDef {
export default class JoinCmd extends Command {
public static description = stripIndent`
Move a local device to an application on another balena server.
Move a local device to a fleet on another balena server.
Move a local device to an application on another balena server, causing
Move a local device to a fleet on another balena server, causing
the device to "join" the new server. The device must be running balenaOS.
For example, you could provision a device against an openBalena installation
where you perform end-to-end tests and then move it to balenaCloud when it's
ready for production.
To move a device between applications on the same server, use the
To move a device between fleets on the same server, use the
\`balena device move\` command instead of \`balena join\`.
If you don't specify a device hostname or IP, this command will automatically
scan the local network for balenaOS devices and prompt you to select one
from an interactive picker. This may require administrator/root privileges.
Likewise, if the application flag is not provided then a picker will be shown.
Likewise, if the fleet option is not provided then a picker will be shown.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
@ -57,10 +59,10 @@ export default class JoinCmd extends Command {
public static examples = [
'$ balena join',
'$ balena join balena.local',
'$ balena join balena.local --application MyApp',
'$ balena join balena.local -a myorg/myapp',
'$ balena join balena.local --fleet MyFleet',
'$ balena join balena.local -f myorg/myfleet',
'$ balena join 192.168.1.25',
'$ balena join 192.168.1.25 --application MyApp',
'$ balena join 192.168.1.25 --fleet MyFleet',
];
public static args = [
@ -75,7 +77,8 @@ export default class JoinCmd extends Command {
public static usage = 'join [deviceIpOrHostname]';
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
...(isV13() ? {} : { application: cf.application }),
fleet: cf.fleet,
pollInterval: flags.integer({
description: 'the interval in minutes to check for updates',
char: 'i',
@ -98,7 +101,7 @@ export default class JoinCmd extends Command {
logger,
sdk,
params.deviceIpOrHostname,
options.application,
options.application || options.fleet,
options.pollInterval,
);
}

View File

@ -31,9 +31,9 @@ interface ArgsDef {
export default class LeaveCmd extends Command {
public static description = stripIndent`
Remove a local device from its balena application.
Remove a local device from its balena fleet.
Remove a local device from its balena application, causing the device to
Remove a local device from its balena fleet, causing the device to
"leave" the server it is provisioned on. This effectively makes the device
"unmanaged". The device must be running balenaOS.

View File

@ -60,50 +60,36 @@ export default class LocalConfigureCmd extends Command {
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
const path = await import('path');
const reconfix = await import('reconfix');
const denymount = promisify(await import('denymount'));
const { safeUmount } = await import('../../utils/helpers');
const { denyMount, safeUmount } = await import('../../utils/umount');
const Logger = await import('../../utils/logger');
const logger = Logger.getLogger();
const configurationSchema = await this.prepareConnectionFile(params.target);
await safeUmount(params.target);
const dmOpts: any = {};
if (process.pkg) {
// when running in a standalone pkg install, the 'denymount'
// executable is placed on the same folder as process.execPath
dmOpts.executablePath = path.join(
path.dirname(process.execPath),
'denymount',
await denyMount(params.target, async () => {
// TODO: safeUmount umounts drives like '/dev/sdc', but does not
// umount image files like 'balena.img'
await safeUmount(params.target);
const config = await reconfix.readConfiguration(
configurationSchema,
params.target,
);
}
const dmHandler = (cb: () => void) =>
reconfix
.readConfiguration(configurationSchema, params.target)
.then(async (config: any) => {
logger.logDebug('Current config:');
logger.logDebug(JSON.stringify(config));
const answers = await this.getConfiguration(config);
logger.logDebug('New config:');
logger.logDebug(JSON.stringify(answers));
if (!answers.hostname) {
await this.removeHostname(configurationSchema);
}
return await reconfix.writeConfiguration(
configurationSchema,
answers,
params.target,
);
})
.asCallback(cb);
await denymount(params.target, dmHandler, dmOpts);
logger.logDebug('Current config:');
logger.logDebug(JSON.stringify(config));
const answers = await this.getConfiguration(config);
logger.logDebug('New config:');
logger.logDebug(JSON.stringify(answers));
if (!answers.hostname) {
await this.removeHostname(configurationSchema);
}
await reconfix.writeConfiguration(
configurationSchema,
answers,
params.target,
);
});
console.log('Done!');
}

View File

@ -20,12 +20,7 @@ import type { BlockDevice } from 'etcher-sdk/build/source-destination';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import {
getChalk,
getCliForm,
getVisuals,
stripIndent,
} from '../../utils/lazy';
import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
yes: boolean;
@ -83,8 +78,9 @@ export default class LocalFlashCmd extends Command {
try {
const info = await execAsync('cat /proc/version');
distroVersion = info.stdout.toLowerCase();
// tslint:disable-next-line: no-empty
} catch {}
} catch {
// pass
}
if (distroVersion.includes('microsoft')) {
throw new ExpectedError(stripIndent`
This command is known not to work on WSL. Please use a CLI release
@ -92,24 +88,15 @@ export default class LocalFlashCmd extends Command {
}
}
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const drive = await this.getDrive(options);
const yes =
options.yes ||
(await getCliForm().ask({
message: 'This will erase the selected drive. Are you sure?',
type: 'confirm',
name: 'yes',
default: false,
}));
if (!yes) {
console.log(getChalk().red.bold('Aborted image flash'));
process.exit(0);
}
const { confirm } = await import('../../utils/patterns');
await confirm(
options.yes,
'This will erase the selected drive. Are you sure?',
);
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const file = new sourceDestination.File({
path: params.image,
});

View File

@ -23,7 +23,12 @@ 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';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
const CONNECTIONS_FOLDER = '/system-connections';
@ -31,6 +36,7 @@ interface FlagsDef {
advanced?: boolean;
application?: string;
app?: string;
fleet?: string;
config?: string;
'config-app-update-poll-interval'?: number;
'config-network'?: string;
@ -66,8 +72,8 @@ export default class OsConfigureCmd extends Command {
public static description = stripIndent`
Configure a previously downloaded balenaOS image.
Configure a previously downloaded balenaOS image for a specific device type or
balena application.
Configure a previously downloaded balenaOS image for a specific device type
or fleet.
Configuration settings such as WiFi authentication will be taken from the
following sources, in precedence order:
@ -75,8 +81,8 @@ export default class OsConfigureCmd extends Command {
2. A given \`config.json\` file specified with the \`--config\` option.
3. User input through interactive prompts (text menus).
The --device-type option may be used to override the application's default
device type, in case of an application with mixed device types.
The --device-type option may be used to override the fleet's default device
type, in case of a fleet with mixed device types.
The --system-connection (-c) option can be used to inject NetworkManager connection
profiles for additional network interfaces, such as cellular/GSM or additional
@ -98,11 +104,10 @@ export default class OsConfigureCmd extends Command {
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',
'$ balena os configure ../path/rpi3.img --fleet myorg/myfleet',
'$ balena os configure ../path/rpi3.img --fleet MyFleet --version 2.12.7',
'$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3',
'$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3 --config myWifiConfig.json',
];
public static args = [
@ -121,15 +126,29 @@ export default class OsConfigureCmd extends Command {
description:
'ask advanced configuration questions (when in interactive mode)',
}),
application: { ...cf.application, exclusive: ['app', 'device'] },
app: { ...cf.app, exclusive: ['application', 'device'] },
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device'],
},
config: flags.string({
description:
'path to a pre-generated config.json file to be injected in the OS image',
}),
'config-app-update-poll-interval': flags.integer({
description:
'interval (in minutes) for the on-device balena supervisor periodic app update check',
'supervisor cloud polling interval in minutes (e.g. for variable updates)',
}),
'config-network': flags.string({
description: 'device network type (non-interactive configuration)',
@ -141,7 +160,7 @@ export default class OsConfigureCmd extends Command {
'config-wifi-ssid': flags.string({
description: 'WiFi SSID (network name) (non-interactive configuration)',
}),
device: { exclusive: ['app', 'application'], ...cf.device },
device: { exclusive: ['app', 'application', 'fleet'], ...cf.device },
'device-api-key': flags.string({
char: 'k',
description:
@ -149,7 +168,7 @@ export default class OsConfigureCmd extends Command {
}),
'device-type': flags.string({
description:
'device type slug (e.g. "raspberrypi3") to override the application device type',
'device type slug (e.g. "raspberrypi3") to override the fleet device type',
}),
'initial-device-name': flags.string({
description:
@ -172,9 +191,10 @@ export default class OsConfigureCmd extends Command {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
OsConfigureCmd,
);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
await validateOptions(options);
@ -302,12 +322,12 @@ async function validateOptions(options: FlagsDef) {
// flag definitions above, so oclif will enforce that they are not both used together.
if (!options.device && !options.application) {
throw new ExpectedError(
"Either the '--device' or the '--application' option must be provided",
"Either the '--device' or the '--fleet' option must be provided",
);
}
if (!options.application && options['device-type']) {
throw new ExpectedError(
"The '--device-type' option can only be used in conjunction with the '--application' option",
"The '--device-type' option can only be used in conjunction with the '--fleet' option",
);
}
if (options['device-api-key']) {
@ -366,7 +386,7 @@ async function checkDeviceTypeCompatibility(
const helpers = await import('../../utils/helpers');
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
throw new ExpectedError(
`Device type ${options['device-type']} is incompatible with application ${options.application}`,
`Device type ${options['device-type']} is incompatible with fleet ${options.application}`,
);
}
}

View File

@ -95,6 +95,11 @@ export default class OsDownloadCmd extends Command {
const { downloadOSImage } = await import('../../utils/cloud');
await downloadOSImage(params.type, options.output, options.version);
try {
await downloadOSImage(params.type, options.output, options.version);
} catch (e) {
e.deviceTypeSlug = params.type;
throw e;
}
}
}

View File

@ -74,9 +74,7 @@ export default class OsInitializeCmd extends Command {
OsInitializeCmd,
);
const { getManifest, safeUmount, sudo } = await import(
'../../utils/helpers'
);
const { getManifest, sudo } = await import('../../utils/helpers');
console.info(`Initializing device ${INIT_WARNING_MESSAGE}`);
@ -94,8 +92,8 @@ export default class OsInitializeCmd extends Command {
options.yes,
`This will erase ${answers.drive}. Are you sure?`,
`Going to erase ${answers.drive}.`,
true,
);
const { safeUmount } = await import('../../utils/umount');
await safeUmount(answers.drive);
}
@ -108,6 +106,7 @@ export default class OsInitializeCmd extends Command {
]);
if (answers.drive != null) {
const { safeUmount } = await import('../../utils/umount');
await safeUmount(answers.drive);
console.info(`You can safely remove ${answers.drive} now`);
}

View File

@ -55,10 +55,8 @@ export default class OsVersionsCmd extends Command {
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(OsVersionsCmd);
const {
versions: vs,
recommended,
} = await getBalenaSdk().models.os.getSupportedVersions(params.type);
const { versions: vs, recommended } =
await getBalenaSdk().models.os.getSupportedVersions(params.type);
vs.forEach((v) => {
console.log(`v${v}` + (v === recommended ? ' (recommended)' : ''));

View File

@ -15,8 +15,8 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import {
getBalenaSdk,
@ -24,23 +24,24 @@ import {
getVisuals,
stripIndent,
} from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../utils/messages';
import type { DockerConnectionCliFlags } from '../utils/docker';
import { dockerConnectionCliFlags } from '../utils/docker';
import * as _ from 'lodash';
import type {
Application,
BalenaSDK,
DeviceTypeJson,
PineExpand,
Release,
} from 'balena-sdk';
import type { Preloader } from 'balena-preload';
import { parseAsInteger } from '../utils/validation';
import { ExpectedError } from '../errors';
import { isV13 } from '../utils/version';
import { flags } from '@oclif/command';
import * as _ from 'lodash';
import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk';
import type { Preloader } from 'balena-preload';
interface FlagsDef extends DockerConnectionCliFlags {
app?: string;
fleet?: string;
commit?: string;
'splash-image'?: string;
'dont-check-arch': boolean;
@ -56,16 +57,16 @@ interface ArgsDef {
export default class PreloadCmd extends Command {
public static description = stripIndent`
Preload an app on a disk image (or Edison zip archive).
Preload a release on a disk image (or Edison zip archive).
Preload a balena application release (app images/containers), and optionally
Preload a release (service images/containers) from a balena fleet, and optionally
a balenaOS splash screen, in a previously downloaded '.img' balenaOS image file
in the local disk (a zip file is only accepted for the Intel Edison device type).
After preloading, the balenaOS image file can be flashed to a device's SD card.
When the device boots, it will not need to download the application, as it was
When the device boots, it will not need to download the release, as it was
preloaded. This is usually combined with release pinning
(https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/)
to avoid the device dowloading a newer release straight away, if one is available.
to avoid the device downloading a newer release straight away, if available.
Check also the Preloading and Preregistering section of the balena CLI's advanced
masterclass document:
https://www.balena.io/docs/learn/more/masterclasses/advanced-cli/#5-preloading-and-preregistering
@ -82,8 +83,8 @@ export default class PreloadCmd extends Command {
`;
public static examples = [
'$ 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 --fleet MyFleet --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0',
'$ balena preload balena.img --fleet myorg/myfleet --splash-image image.png',
'$ balena preload balena.img',
];
@ -98,13 +99,16 @@ export default class PreloadCmd extends Command {
public static usage = 'preload <image>';
public static flags: flags.Input<FlagsDef> = {
// TODO: Replace with application/a in #v13?
app: cf.application,
...(isV13() ? {} : { app: cf.application }),
fleet: cf.fleet,
commit: flags.string({
description: `\
The commit hash for a specific application release to preload, use "current" to specify the current
release (ignored if no appId is given). The current release is usually also the latest, but can be
manually pinned using https://github.com/balena-io-projects/staged-releases .\
The commit hash of the release to preload. Use "current" to specify the current
release (ignored if no appId is given). The current release is usually also the
latest, but can be pinned to a specific release. See:
https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/
https://www.balena.io/docs/learn/more/masterclasses/fleet-management/#63-pin-using-the-api
https://github.com/balena-io-examples/staged-releases\
`,
char: 'c',
}),
@ -115,7 +119,7 @@ manually pinned using https://github.com/balena-io-projects/staged-releases .\
'dont-check-arch': flags.boolean({
default: false,
description:
'disables check for matching architecture in image and application',
'disable architecture compatibility check between image and fleet',
}),
'pin-device-to-release': flags.boolean({
default: false,
@ -159,6 +163,11 @@ Can be repeated to add multiple certificates.\
PreloadCmd,
);
if (options.app && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.app ||= options.fleet;
const balena = getBalenaSdk();
const balenaPreload = await import('balena-preload');
const visuals = getVisuals();
@ -169,6 +178,14 @@ Can be repeated to add multiple certificates.\
try {
const fs = await import('fs');
await fs.promises.access(params.image);
const path = await import('path');
if (path.extname(params.image) === '.zip') {
console.warn(stripIndent`
------------------------------------------------------------------------------
Warning: A zip file is only accepted for the Intel Edison device type.
------------------------------------------------------------------------------
`);
}
} catch (error) {
throw new ExpectedError(
`The provided image path does not exist: ${params.image}`,
@ -182,7 +199,7 @@ Can be repeated to add multiple certificates.\
const { getApplication } = await import('../utils/sdk');
const application = await getApplication(balena, options.app);
if (!application) {
throw new ExpectedError(`Application not found: ${options.app}`);
throw new ExpectedError(`Fleet not found: ${options.app}`);
}
options.app = application.slug;
}
@ -231,7 +248,7 @@ Can be repeated to add multiple certificates.\
if (dontCheckArch && !appId) {
throw new ExpectedError(
'You need to specify an application if you disable the architecture check.',
'You need to specify a fleet if you disable the architecture check.',
);
}
@ -308,7 +325,6 @@ Can be repeated to add multiple certificates.\
readonly applicationExpandOptions: PineExpand<Application> = {
owns__release: {
$select: ['id', 'commit', 'end_timestamp', 'composition'],
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
$expand: {
contains__image: {
$select: ['image'],
@ -322,77 +338,75 @@ Can be repeated to add multiple certificates.\
$filter: {
status: 'success',
},
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
},
should_be_running__release: {
$select: 'commit',
},
};
allDeviceTypes: DeviceTypeJson.DeviceType[];
async getDeviceTypes() {
if (this.allDeviceTypes === undefined) {
const balena = getBalenaSdk();
const deviceTypes = await balena.models.config.getDeviceTypes();
this.allDeviceTypes = _.sortBy(deviceTypes, 'name');
}
return this.allDeviceTypes;
}
isCurrentCommit(commit: string) {
return commit === 'latest' || commit === 'current';
}
async getDeviceTypesWithSameArch(deviceTypeSlug: string) {
const deviceTypes = await this.getDeviceTypes();
const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug });
if (!deviceType) {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value();
}
async getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) {
const balena = getBalenaSdk();
const deviceTypes = await this.getDeviceTypesWithSameArch(deviceTypeSlug);
// TODO: remove the explicit types once https://github.com/balena-io/balena-sdk/pull/889 gets merged
return balena.pine.get<
Application,
Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
}
>
>({
resource: 'my_application',
options: {
$filter: {
is_for__device_type: {
$any: {
$alias: 'dt',
$expr: {
dt: {
slug: { $in: deviceTypes },
},
},
},
},
owns__release: {
$any: {
$alias: 'r',
$expr: {
r: {
status: 'success',
try {
await balena.models.deviceType.get(deviceTypeSlug);
} catch {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return (await balena.models.application.getAll({
$select: ['id', 'app_name', 'should_track_latest_release'],
$expand: this.applicationExpandOptions,
$filter: {
// get the apps that are of the same arch as the device type of the image
is_for__device_type: {
$any: {
$alias: 'dt',
$expr: {
dt: {
is_of__cpu_architecture: {
$any: {
$alias: 'ioca',
$expr: {
ioca: {
is_supported_by__device_type: {
$any: {
$alias: 'isbdt',
$expr: {
isbdt: {
slug: deviceTypeSlug,
},
},
},
},
},
},
},
},
},
},
},
},
$expand: this.applicationExpandOptions,
$select: ['id', 'app_name', 'should_track_latest_release'],
$orderby: 'app_name asc',
owns__release: {
$any: {
$alias: 'r',
$expr: {
r: {
status: 'success',
},
},
},
},
},
});
$orderby: 'app_name asc',
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
}
>;
}
async selectApplication(deviceTypeSlug: string) {
@ -409,11 +423,11 @@ Can be repeated to add multiple certificates.\
applicationInfoSpinner.stop();
if (applications.length === 0) {
throw new ExpectedError(
`You have no apps with successful releases for a '${deviceTypeSlug}' device type.`,
`No fleets found with successful releases for device type '${deviceTypeSlug}'`,
);
}
return getCliForm().ask({
message: 'Select an application',
message: 'Select a fleet',
type: 'list',
choices: applications.map((app) => ({
name: app.app_name,
@ -424,7 +438,7 @@ Can be repeated to add multiple certificates.\
selectApplicationCommit(releases: Release[]) {
if (releases.length === 0) {
throw new ExpectedError('This application has no successful releases.');
throw new ExpectedError('This fleet has no successful releases.');
}
const DEFAULT_CHOICE = { name: 'current', value: 'current' };
const choices = [DEFAULT_CHOICE].concat(
@ -457,23 +471,23 @@ Can be repeated to add multiple certificates.\
}
const message = `\
This application is set to track the latest release, and non-pinned devices
This fleet is set to track the latest release, and non-pinned devices
are automatically updated when a new release is available. This may lead to
unexpected behavior: The preloaded device will download and install the latest
release once it is online.
This prompt gives you the opportunity to disable automatic updates for this
application now. Note that this would result in the application being pinned
to the current latest release, rather than some other release that may have
This prompt gives you the opportunity to disable automatic updates for
this fleet now. Note that this would result in the fleet being pinned to
the current latest release, rather than some other release that may have
been selected for preloading. The pinned released may be further managed
through the web dashboard or programatically through the balena API / SDK.
Documentation about release policies and app/device pinning can be found at:
Documentation about release policies and pinning can be found at:
https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/
Alternatively, the --pin-device-to-release flag may be used to pin only the
preloaded device to the selected release.
Would you like to disable automatic updates for this application now?\
Would you like to disable automatic updates for this fleet now?\
`;
const update = await getCliForm().ask({
message,
@ -523,7 +537,7 @@ Would you like to disable automatic updates for this application now?\
if (this.isCurrentCommit(options.commit)) {
if (!appCommit) {
throw new Error(
`Unexpected empty commit hash for app ID "${application.id}"`,
`Unexpected empty commit hash for fleet ID "${application.id}"`,
);
}
// handle `--commit current` (and its `--commit latest` synonym)

View File

@ -36,7 +36,7 @@ enum BuildTarget {
}
interface ArgsDef {
applicationOrDevice: string;
fleetOrDevice: string;
}
interface FlagsDef {
@ -47,8 +47,8 @@ interface FlagsDef {
pull: boolean;
'noparent-check': boolean;
'registry-secrets'?: string;
gitignore?: boolean;
nogitignore?: boolean;
gitignore?: boolean; // v13: delete this flag
nogitignore?: boolean; // v13: delete this flag
nolive: boolean;
detached: boolean;
service?: string[];
@ -58,14 +58,15 @@ interface FlagsDef {
'noconvert-eol': boolean;
'multi-dockerignore': boolean;
'release-tag'?: string[];
draft: boolean;
help: void;
}
export default class PushCmd extends Command {
public static description = stripIndent`
Start a build on the remote balenaCloud build servers, or a local mode device.
Build release images on balenaCloud servers or on a local mode device.
Start a build on the remote balenaCloud build servers, or a local mode device.
Build release images on balenaCloud servers or on 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,16 +93,16 @@ export default class PushCmd extends Command {
${dockerignoreHelp.split('\n').join('\n\t\t')}
Note: the --service and --env flags must come after the applicationOrDevice
Note: the --service and --env flags must come after the fleetOrDevice
parameter, as per examples.
`;
public static examples = [
'$ 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 myFleet',
'$ balena push myFleet --source <source directory>',
'$ balena push myFleet -s <source directory>',
'$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"',
'$ balena push myorg/myfleet',
'',
'$ balena push 10.0.0.1',
'$ balena push 10.0.0.1 --source <source directory>',
@ -115,15 +116,15 @@ export default class PushCmd extends Command {
public static args = [
{
name: 'applicationOrDevice',
name: 'fleetOrDevice',
description:
'application name or slug, or local device IP address or hostname',
'fleet name or slug, or local device IP address or ".local" hostname',
required: true,
parse: lowercaseIfSlug,
},
];
public static usage = 'push <applicationOrDevice>';
public static usage = 'push <fleetOrDevice>';
public static flags: flags.Input<FlagsDef> = {
source: flags.string({
@ -188,7 +189,7 @@ export default class PushCmd extends Command {
When pushing to the cloud, this option will cause the build to start, then
return execution back to the shell, with the status and release ID (if
applicable). When pushing to a local mode device, this option will cause
the command to not tail application logs when the build has completed.`,
the command to not tail logs when the build has completed.`,
char: 'd',
default: false,
}),
@ -236,11 +237,20 @@ export default class PushCmd extends Command {
'Have each service use its own .dockerignore file. See "balena help push".',
char: 'm',
default: false,
exclusive: ['gitignore'],
exclusive: ['gitignore'], // v13: delete this line
}),
...(isV13()
? {}
: {
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.`,
char: 'g',
default: false,
exclusive: ['multi-dockerignore'],
}),
nogitignore: flags.boolean({
description:
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
@ -249,24 +259,23 @@ export default class PushCmd extends Command {
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.`,
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
Set release tags if the image build is successful (balenaCloud only). 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'],
}),
draft: flags.boolean({
description: stripIndent`
Instruct the builder to create the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.`,
default: false,
}),
help: cf.help,
};
@ -292,14 +301,12 @@ export default class PushCmd extends Command {
},
);
switch (await this.getBuildTarget(params.applicationOrDevice)) {
switch (await this.getBuildTarget(params.fleetOrDevice)) {
case BuildTarget.Cloud:
logger.logDebug(
`Pushing to cloud for application: ${params.applicationOrDevice}`,
);
logger.logDebug(`Pushing to cloud for fleet: ${params.fleetOrDevice}`);
await this.pushToCloud(
params.applicationOrDevice,
params.fleetOrDevice,
options,
sdk,
dockerfilePath,
@ -308,11 +315,9 @@ export default class PushCmd extends Command {
break;
case BuildTarget.Device:
logger.logDebug(
`Pushing to local device: ${params.applicationOrDevice}`,
);
logger.logDebug(`Pushing to local device: ${params.fleetOrDevice}`);
await this.pushToDevice(
params.applicationOrDevice,
params.fleetOrDevice,
options,
dockerfilePath,
registrySecrets,
@ -366,13 +371,14 @@ export default class PushCmd extends Command {
registrySecrets,
headless: options.detached,
convertEol: !options['noconvert-eol'],
isDraft: options.draft,
};
const args = {
appSlug: application.slug,
source: options.source,
auth: token,
baseUrl,
nogitignore: !options.gitignore,
nogitignore: !options.gitignore, // v13: delete this line
sdk,
opts,
};
@ -398,11 +404,11 @@ export default class PushCmd extends Command {
registrySecrets: RegistrySecrets,
) {
// Check for invalid options
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag'];
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag', 'draft'];
this.checkInvalidOptions(
remoteOnlyOptions,
options,
'is only valid when pushing to an application',
'is only valid when pushing to a fleet',
);
const deviceDeploy = await import('../utils/device/deploy');
@ -416,7 +422,7 @@ export default class PushCmd extends Command {
multiDockerignore: options['multi-dockerignore'],
nocache: options.nocache,
pull: options.pull,
nogitignore: !options.gitignore,
nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'],
nolive: options.nolive,
detached: options.detached,

View File

@ -0,0 +1,86 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseFinalizeCmd extends Command {
public static description = stripIndent`
Finalize a release.
Finalize a release. Releases can be "draft" or "final", and this command
changes a draft release into a final release. Draft releases can be created
with the \`--draft\` option of the \`balena build\` or \`balena deploy\`
commands.
Draft releases are not automatically deployed to devices tracking the latest
release. For a draft release to be deployed to a device, the device should be
explicity pinned to that release. Conversely, final releases may trigger immediate
deployment to unpinned devices (subject to a device's polling period) and, for
this reason, final releases cannot be changed back to draft status.
`;
public static examples = [
'$ balena release finalize a777f7345fe3d655c1c981aa642e5555',
'$ balena release finalize 1234567',
];
public static usage = 'release finalize <commitOrId>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to finalize',
required: true,
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleaseFinalizeCmd);
const balena = getBalenaSdk();
const release = await balena.models.release.get(params.commitOrId, {
$select: ['id', 'is_final'],
});
if (release.is_final) {
console.log(`Release ${params.commitOrId} is already finalized!`);
return;
}
await balena.models.release.finalize(release.id);
console.log(`Release ${params.commitOrId} finalized`);
}
}

View File

@ -0,0 +1,128 @@
/**
* @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';
import type * as BalenaSdk from 'balena-sdk';
import jsyaml = require('js-yaml');
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
composition?: boolean;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseCmd extends Command {
public static description = stripIndent`
Get info for a release.
`;
public static examples = [
'$ balena release a777f7345fe3d655c1c981aa642e5555',
'$ balena release 1234567',
];
public static usage = 'release <commitOrId>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
composition: flags.boolean({
default: false,
char: 'c',
description: 'Return the release composition',
}),
};
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to get information',
required: true,
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ReleaseCmd,
);
const balena = getBalenaSdk();
if (options.composition) {
await this.showComposition(params.commitOrId, balena);
} else {
await this.showReleaseInfo(params.commitOrId, balena);
}
}
async showComposition(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
const release = await balena.models.release.get(commitOrId, {
$select: 'composition',
});
console.log(jsyaml.dump(release.composition));
}
async showReleaseInfo(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
'build_log',
'start_timestamp',
'end_timestamp',
];
const release = await balena.models.release.get(commitOrId, {
$select: fields,
$expand: {
release_tag: {
$select: ['tag_key', 'value'],
},
},
});
const tagStr = release
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
.join('\n');
const _ = await import('lodash');
const values = _.mapValues(
release,
(val) => val ?? 'N/a',
) as Dictionary<string>;
values['tags'] = tagStr;
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
}
}

86
lib/commands/releases.ts Normal file
View File

@ -0,0 +1,86 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationNameNote } from '../utils/messages';
import type * as BalenaSdk from 'balena-sdk';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class ReleasesCmd extends Command {
public static description = stripIndent`
List all releases of a fleet.
List all releases of the given fleet.
${applicationNameNote.split('\n').join('\n\t\t')}
`;
public static examples = ['$ balena releases myorg/myfleet'];
public static usage = 'releases <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = [
{
name: 'fleet',
description: 'fleet name or slug',
required: true,
},
];
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
];
const balena = getBalenaSdk();
const releases = await balena.models.release.getAllByApplication(
params.fleet,
{ $select: fields },
);
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
fields,
),
);
}
}

View File

@ -87,9 +87,8 @@ export default class ScanCmd extends Command {
const ux = getCliUx();
ux.action.start('Scanning for local balenaOS devices');
const localDevices: LocalBalenaOsDevice[] = await discover.discoverLocalBalenaOsDevices(
discoverTimeout,
);
const localDevices: LocalBalenaOsDevice[] =
await discover.discoverLocalBalenaOsDevices(discoverTimeout);
const engineReachableDevices: boolean[] = await Promise.all(
localDevices.map(async ({ address }: { address: string }) => {
const docker = await dockerUtils.createClient({

View File

@ -31,20 +31,20 @@ interface FlagsDef {
}
interface ArgsDef {
applicationOrDevice: string;
fleetOrDevice: string;
service?: string;
}
export default class SshCmd extends Command {
public static description = stripIndent`
SSH into the host or application container of a device.
Open a SSH prompt on a device's host OS or service container.
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 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.
If a fleet 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.
For local devices, the IP address and .local domain name are supported.
If the device is referenced by IP or \`.local\` address, the connection
@ -64,7 +64,7 @@ export default class SshCmd extends Command {
`;
public static examples = [
'$ balena ssh MyApp',
'$ balena ssh MyFleet',
'$ balena ssh f49cefd',
'$ balena ssh f49cefd my-service',
'$ balena ssh f49cefd --port <port>',
@ -76,9 +76,9 @@ export default class SshCmd extends Command {
public static args = [
{
name: 'applicationOrDevice',
name: 'fleetOrDevice',
description:
'application name/slug/id, device uuid, or address of local device',
'fleet name/slug/id, device uuid, or address of local device',
required: true,
},
{
@ -88,7 +88,7 @@ export default class SshCmd extends Command {
},
];
public static usage = 'ssh <applicationOrDevice> [service]';
public static usage = 'ssh <fleetOrDevice> [service]';
public static flags: flags.Input<FlagsDef> = {
port: flags.integer({
@ -124,10 +124,10 @@ export default class SshCmd extends Command {
);
// Local connection
if (validateLocalHostnameOrIp(params.applicationOrDevice)) {
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
return await performLocalDeviceSSH({
address: params.applicationOrDevice,
address: params.fleetOrDevice,
port: options.port,
forceTTY: options.tty,
verbose: options.verbose,
@ -136,7 +136,7 @@ export default class SshCmd extends Command {
}
// Remote connection
const { getProxyConfig, which } = await import('../utils/helpers');
const { getProxyConfig } = await import('../utils/helpers');
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
const sdk = getBalenaSdk();
@ -147,7 +147,7 @@ export default class SshCmd extends Command {
await Command.checkLoggedIn();
const deviceUuid = await getOnlineTargetDeviceUuid(
sdk,
params.applicationOrDevice,
params.fleetOrDevice,
);
const device = await sdk.models.device.get(deviceUuid, {
@ -156,6 +156,7 @@ export default class SshCmd extends Command {
const deviceId = device.id;
const supervisorVersion = device.supervisor_version;
const { which } = await import('../utils/which');
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
useProxy ? which('proxytunnel', false) : undefined,
@ -301,7 +302,7 @@ export default class SshCmd extends Command {
// container
const childProcess = await import('child_process');
const { escapeRegExp } = await import('lodash');
const { which } = await import('../utils/helpers');
const { which } = await import('../utils/which');
const { deviceContainerEngineBinary } = await import(
'../utils/device/ssh'
);

View File

@ -20,10 +20,16 @@ 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';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../utils/messages';
import { isV13 } from '../utils/version';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string;
duration?: string;
help: void;
@ -35,16 +41,16 @@ interface ArgsDef {
export default class SupportCmd extends Command {
public static description = stripIndent`
Grant or revoke support access for devices and applications.
Grant or revoke support access for devices or fleets.
Grant or revoke balena support agent access to devices and applications
Grant or revoke balena support agent access to devices or fleets
on balenaCloud. (This command does not apply to openBalena.)
Access will be automatically revoked once the specified duration has elapsed.
Duration defaults to 24h, but can be specified using --duration flag in days
or hours, e.g. '12h', '2d'.
Both --device and --application flags accept multiple values, specified as
Both --device and --fleet flags accept multiple values, specified as
a comma-separated list (with no spaces).
${applicationIdInfo.split('\n').join('\n\t\t')}
@ -52,8 +58,8 @@ export default class SupportCmd extends Command {
public static examples = [
'balena support enable --device ab346f,cd457a --duration 3d',
'balena support enable --application app3 --duration 12h',
'balena support disable -a myorg/myapp',
'balena support enable --fleet myFleet --duration 12h',
'balena support disable -f myorg/myfleet',
];
public static args = [
@ -71,10 +77,11 @@ export default class SupportCmd extends Command {
description: 'comma-separated list (no spaces) of device UUIDs',
char: 'd',
}),
application: {
...cf.application,
...(isV13() ? {} : { application: cf.application }),
fleet: {
...cf.fleet,
description:
'comma-separated list (no spaces) of application names or org/name slugs',
'comma-separated list (no spaces) of fleet names or org/name slugs',
},
duration: flags.string({
description:
@ -91,6 +98,11 @@ export default class SupportCmd extends Command {
SupportCmd,
);
if (options.application && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
const balena = getBalenaSdk();
const ux = getCliUx();
@ -98,9 +110,7 @@ export default class SupportCmd extends Command {
// Validation
if (!options.device && !options.application) {
throw new ExpectedError(
'At least one device or application must be specified',
);
throw new ExpectedError('At least one device or fleet must be specified');
}
if (options.duration != null && !enabling) {
@ -135,10 +145,10 @@ export default class SupportCmd extends Command {
// Process applications
for (const appName of appNames) {
if (enabling) {
ux.action.start(`${enablingMessage} application ${appName}`);
ux.action.start(`${enablingMessage} fleet ${appName}`);
await balena.models.application.grantSupportAccess(appName, expiryTs);
} else if (params.action === 'disable') {
ux.action.start(`${disablingMessage} application ${appName}`);
ux.action.start(`${disablingMessage} fleet ${appName}`);
await balena.models.application.revokeSupportAccess(appName);
}
ux.action.stop();

View File

@ -19,14 +19,20 @@ 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 {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
interface FlagsDef {
app?: string;
application?: string;
fleet?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
interface ArgsDef {
@ -35,16 +41,16 @@ interface ArgsDef {
export default class TagRmCmd extends Command {
public static description = stripIndent`
Remove a tag from an application, device or release.
Remove a tag from a fleet, device or release.
Remove a tag from an application, device or release.
Remove a tag from a fleet, 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 --fleet MyFleet',
'$ balena tag rm myTagKey -f myorg/myfleet',
'$ balena tag rm myTagKey --device 7cf02a6',
'$ balena tag rm myTagKey --release 1234',
'$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6',
@ -61,21 +67,29 @@ export default class TagRmCmd extends Command {
public static usage = 'tag rm <tagKey>';
public static flags: flags.Input<FlagsDef> = {
application: {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'device', 'release'],
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device', 'release'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
exclusive: ['app', 'application', 'fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
exclusive: ['app', 'application', 'fleet', 'device'],
},
help: cf.help,
};
@ -87,9 +101,10 @@ export default class TagRmCmd extends Command {
TagRmCmd,
);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
const balena = getBalenaSdk();
@ -130,9 +145,9 @@ 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 <appNameOrSlug>
* A device, with --device <uuid>
* A release, with --release <id or commit>
* A fleet, with --fleet <fleetNameOrSlug>
* A device, with --device <UUID>
* A release, with --release <ID or commit>
See the help page for examples:

View File

@ -19,14 +19,20 @@ 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 {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
interface FlagsDef {
app?: string;
application?: string;
fleet?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
interface ArgsDef {
@ -36,9 +42,9 @@ interface ArgsDef {
export default class TagSetCmd extends Command {
public static description = stripIndent`
Set a tag on an application, device or release.
Set a tag on a fleet, device or release.
Set a tag on an application, device or release.
Set a tag on a fleet, device or release.
You can optionally provide a value to be associated with the created
tag, as an extra argument after the tag key. If a value isn't
@ -48,9 +54,9 @@ export default class TagSetCmd extends Command {
`;
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 mySimpleTag --fleet MyFleet',
'$ balena tag set mySimpleTag -f myorg/myfleet',
'$ balena tag set myCompositeTag myTagValue --fleet MyFleet',
'$ balena tag set myCompositeTag myTagValue --device 7cf02a6',
'$ balena tag set myCompositeTag "my tag value with whitespaces" --device 7cf02a6',
'$ balena tag set myCompositeTag myTagValue --release 1234',
@ -74,21 +80,29 @@ export default class TagSetCmd extends Command {
public static usage = 'tag set <tagKey> [value]';
public static flags: flags.Input<FlagsDef> = {
application: {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'device', 'release'],
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device', 'release'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
exclusive: ['app', 'application', 'fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
exclusive: ['app', 'application', 'fleet', 'device'],
},
help: cf.help,
};
@ -100,9 +114,10 @@ export default class TagSetCmd extends Command {
TagSetCmd,
);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
const balena = getBalenaSdk();
@ -151,9 +166,9 @@ 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 <appNameOrSlug>
* A device, with --device <uuid>
* A release, with --release <id or commit>
* A fleet, with --fleet <fleetNameOrSlug>
* A device, with --device <UUID>
* A release, with --release <ID or commit>
See the help page for examples:

View File

@ -20,29 +20,34 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../utils/messages';
import { isV13 } from '../utils/version';
interface FlagsDef {
app?: string;
application?: string;
fleet?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
export default class TagsCmd extends Command {
public static description = stripIndent`
List all tags for an application, device or release.
List all tags for a fleet, device or release.
List all tags and their values for a particular application,
device or release.
List all tags and their values for the specified fleet, device or release.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena tags --application MyApp',
'$ balena tags -a myorg/myapp',
'$ balena tags --fleet MyFleet',
'$ balena tags -f myorg/myfleet',
'$ balena tags --device 7cf02a6',
'$ balena tags --release 1234',
'$ balena tags --release b376b0e544e9429483b656490e5b9443b4349bd6',
@ -51,21 +56,29 @@ export default class TagsCmd extends Command {
public static usage = 'tags';
public static flags: flags.Input<FlagsDef> = {
application: {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'device', 'release'],
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device', 'release'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
exclusive: ['app', 'application', 'fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
exclusive: ['app', 'application', 'fleet', 'device'],
},
help: cf.help,
};
@ -75,9 +88,10 @@ export default class TagsCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
const balena = getBalenaSdk();
@ -123,7 +137,7 @@ export default class TagsCmd extends Command {
protected missingResourceMessage = stripIndent`
To list tags for a resource, you must provide exactly one of:
* An application, with --application <appNameOrSlug>
* A fleet, with --fleet <fleetNameOrSlug>
* A device, with --device <uuid>
* A release, with --release <id or commit>

View File

@ -24,6 +24,8 @@ import {
} from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { lowercaseIfSlug } from '../utils/normalization';
import type { Server, Socket } from 'net';
interface FlagsDef {
@ -32,7 +34,7 @@ interface FlagsDef {
}
interface ArgsDef {
deviceOrApplication: string;
deviceOrFleet: string;
}
export default class TunnelCmd extends Command {
@ -62,7 +64,7 @@ export default class TunnelCmd extends Command {
public static examples = [
'# map remote port 22222 to localhost:22222',
'$ balena tunnel myApp -p 22222',
'$ balena tunnel myFleet -p 22222',
'',
'# map remote port 22222 to localhost:222',
'$ balena tunnel 2ead211 -p 22222:222',
@ -71,21 +73,22 @@ export default class TunnelCmd extends Command {
'$ balena tunnel 1546690 -p 22222:0.0.0.0',
'',
'# map remote port 22222 to any address on your host machine, port 222',
'$ balena tunnel myApp -p 22222:0.0.0.0:222',
'$ balena tunnel myFleet -p 22222:0.0.0.0:222',
'',
'# multiple port tunnels can be specified at any one time',
'$ balena tunnel myApp -p 8080:3000 -p 8081:9000',
'$ balena tunnel myFleet -p 8080:3000 -p 8081:9000',
];
public static args = [
{
name: 'deviceOrApplication',
description: 'device uuid or application name/slug/id',
name: 'deviceOrFleet',
description: 'device UUID or fleet name/slug/ID',
required: true,
parse: lowercaseIfSlug,
},
];
public static usage = 'tunnel <deviceOrApplication>';
public static usage = 'tunnel <deviceOrFleet>';
public static flags: flags.Input<FlagsDef> = {
port: flags.string({
@ -132,10 +135,7 @@ export default class TunnelCmd extends Command {
// Ascertain device uuid
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
const uuid = await getOnlineTargetDeviceUuid(
sdk,
params.deviceOrApplication,
);
const uuid = await getOnlineTargetDeviceUuid(sdk, params.deviceOrFleet);
const device = await sdk.models.device.get(uuid);
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);

230
lib/deprecation.ts Normal file
View File

@ -0,0 +1,230 @@
/**
* @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.
*/
export interface ReleaseTimestampsByVersion {
[version: string]: string; // e.g. { '12.0.0': '2021-06-16T12:54:52.000Z' }
lastFetched: string; // ISO 8601 timestamp, e.g. '2021-06-27T16:46:10.000Z'
}
/**
* Warn about and enforce the CLI deprecation policy stated in the README
* file. In particular:
* The latest release of a major version will remain compatible with
* the backend services for at least one year from the date when the
* following major version is released. [...]
* Half way through to that period (6 months), old major versions of the
* balena CLI will start printing a deprecation warning message.
* At the end of that period, older major versions will abort with an error
* message unless the `--unsupported` flag is used.
*
* - Check for new balena-cli releases by querying the npm registry.
* - Cache results for a number of days to improve performance.
*
* For this feature's specification and planning, see (restricted access):
* https://jel.ly.fish/ed8d2395-9323-418c-bb67-d11d32a17d00
*/
export class DeprecationChecker {
readonly majorVersionFetchIntervalDays = 7;
readonly expiryDays = 365;
readonly deprecationDays = Math.ceil(this.expiryDays / 2);
readonly msInDay = 24 * 60 * 60 * 1000; // milliseconds in a day
readonly debugPrefix = 'Deprecation check';
readonly cacheFile = 'cachedReleaseTimestamps';
readonly now = new Date().getTime();
private initialized = false;
storage: ReturnType<typeof import('balena-settings-storage')>;
cachedTimestamps: ReleaseTimestampsByVersion;
nextMajorVersion: string; // semver without the 'v' prefix
constructor(protected currentVersion: string) {
const semver = require('semver') as typeof import('semver');
const major = semver.major(this.currentVersion, { loose: true });
this.nextMajorVersion = `${major + 1}.0.0`;
}
public async init() {
if (this.initialized) {
return;
}
this.initialized = true;
const settings = await import('balena-settings-client');
const getStorage = await import('balena-settings-storage');
const dataDirectory = settings.get<string>('dataDirectory');
this.storage = getStorage({ dataDirectory });
let stored: ReleaseTimestampsByVersion | undefined;
try {
stored = (await this.storage.get(
this.cacheFile,
)) as ReleaseTimestampsByVersion;
} catch {
// ignore
}
this.cachedTimestamps = {
...stored,
// '1970-01-01T00:00:00.000Z' is new Date(0).toISOString()
lastFetched: stored?.lastFetched || '1970-01-01T00:00:00.000Z',
};
}
/**
* Get NPM registry URL to retrieve the package.json file for a given version.
* @param version Semver without 'v' prefix, e.g. '12.0.0.'
*/
protected getNpmUrl(version: string) {
return `http://registry.npmjs.org/balena-cli/${version}`;
}
/**
* Query the npm registry (HTTP request) for a given balena-cli version.
*
* @param version semver version without the 'v' prefix, e.g. '13.0.0'
* @returns `undefined` if the request status code is 404 (version not
* published), otherwise a publishedAt date in ISO 8601 format, e.g.
* '2021-06-27T16:46:10.000Z'.
*/
protected async fetchPublishedTimestampForVersion(
version: string,
): Promise<string | undefined> {
const { default: got } = await import('got');
const url = this.getNpmUrl(version);
let response: import('got').Response<Dictionary<any>> | undefined;
try {
response = await got(url, { responseType: 'json', retry: 0 });
} catch (e) {
// 404 is expected if `version` hasn't been published yet
if (e.response?.statusCode !== 404) {
throw new Error(`Failed to query "${url}":\n${e}`);
}
}
// response.body looks like a package.json file, plus possibly a
// `versionist.publishedAt` field added by `github.com/product-os/versionist`
const publishedAt: string | undefined =
response?.body?.versionist?.publishedAt;
if (!publishedAt && process.env.DEBUG) {
console.error(`\
[debug] ${this.debugPrefix}: balena CLI next major version "${this.nextMajorVersion}" not released, \
or release date not available`);
}
return publishedAt; // ISO 8601, e.g. '2021-06-27T16:46:10.000Z'
}
/**
* Check if we already know (cached value) when the next major version
* was released. If we don't know, check how long ago the npm registry
* was last fetched, and fetch again if it has been longer than
* `majorVersionFetchIntervalDays`.
*/
public async checkForNewReleasesIfNeeded() {
if (process.env.BALENARC_UNSUPPORTED) {
return; // for the benefit of code testing
}
await this.init();
if (this.cachedTimestamps[this.nextMajorVersion]) {
// A cached value exists: no need to check the npm registry
return;
}
const lastFetched = new Date(this.cachedTimestamps.lastFetched).getTime();
const daysSinceLastFetch = (this.now - lastFetched) / this.msInDay;
if (daysSinceLastFetch < this.majorVersionFetchIntervalDays) {
if (process.env.DEBUG) {
// toFixed(5) results in a precision of ~1 second
const days = daysSinceLastFetch.toFixed(5);
console.error(`\
[debug] ${this.debugPrefix}: ${days} days since last npm registry query for next major version release date.
[debug] Will not query the registry again until at least ${this.majorVersionFetchIntervalDays} days have passed.`);
}
return;
}
if (process.env.DEBUG) {
console.error(`\
[debug] ${
this.debugPrefix
}: Cache miss for the balena CLI next major version release date.
[debug] Will query ${this.getNpmUrl(this.nextMajorVersion)}`);
}
try {
const publishedAt = await this.fetchPublishedTimestampForVersion(
this.nextMajorVersion,
);
if (publishedAt) {
this.cachedTimestamps[this.nextMajorVersion] = publishedAt;
}
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] ${this.debugPrefix}: ${e}`);
}
}
// Refresh `lastFetched` regardless of whether or not the request to the npm
// registry was successful. Will try again after `majorVersionFetchIntervalDays`.
this.cachedTimestamps.lastFetched = new Date(this.now).toISOString();
await this.storage.set(this.cacheFile, this.cachedTimestamps);
}
/**
* Use previously cached data (local cache only, fast execution) to check
* whether this version of the CLI is deprecated as per deprecation policy,
* in which case warn about it and conditionally throw an error.
*/
public async warnAndAbortIfDeprecated() {
if (process.env.BALENARC_UNSUPPORTED) {
return; // for the benefit of code testing
}
await this.init();
const nextMajorDateStr = this.cachedTimestamps[this.nextMajorVersion];
if (!nextMajorDateStr) {
return;
}
const nextMajorDate = new Date(nextMajorDateStr).getTime();
const daysElapsed = Math.trunc((this.now - nextMajorDate) / this.msInDay);
if (daysElapsed > this.expiryDays) {
const { ExpectedError } = await import('./errors');
throw new ExpectedError(this.getExpiryMsg(daysElapsed));
} else if (daysElapsed > this.deprecationDays && process.stderr.isTTY) {
console.error(this.getDeprecationMsg(daysElapsed));
}
}
/** Separate function for the benefit of code testing */
getDeprecationMsg(daysElapsed: number) {
const { warnify } =
require('./utils/messages') as typeof import('./utils/messages');
return warnify(`\
CLI version ${this.nextMajorVersion} was released ${daysElapsed} days ago: please upgrade.
This version of the balena CLI (${this.currentVersion}) will exit with an error
message after ${this.expiryDays} days from the release of version ${this.nextMajorVersion},
as per deprecation policy: https://git.io/JRHUW#deprecation-policy
The --unsupported flag may be used to bypass this deprecation check and
allow the CLI to keep working beyond the deprecation period. However,
note that the balenaCloud or openBalena backends may be updated in a way
that is no longer compatible with this version.`);
}
/** Separate function the benefit of code testing */
getExpiryMsg(daysElapsed: number) {
return `
This version of the balena CLI (${this.currentVersion}) has expired: please upgrade.
${daysElapsed} days have passed since the release of CLI version ${this.nextMajorVersion}.
See deprecation policy at: https://git.io/JRHUW#deprecation-policy
The --unsupported flag may be used to bypass this deprecation check and
continue using this version of the CLI. However, note that the balenaCloud
or openBalena backends may be updated in a way that is no longer compatible
with this CLI version.`;
}
}

View File

@ -174,6 +174,13 @@ const messages: {
BalenaExpiredToken: () => stripIndent`
Looks like the session token has expired.
Try logging in again with the "balena login" command.`,
BalenaInvalidDeviceType: (error: Error & { deviceTypeSlug?: string }) => {
const slug = error.deviceTypeSlug ? `"${error.deviceTypeSlug}"` : 'slug';
return stripIndent`
Device type ${slug} not recognized. Perhaps misspelled?
Check available device types with "balena devices supported"`;
},
};
// TODO remove these regexes when we have a way of uniquely indentifying errors.
@ -279,24 +286,3 @@ export const printErrorMessage = function (message: string) {
export const printExpectedErrorMessage = function (message: string) {
console.error(`${message}\n`);
};
/**
* Print a friendly error message and exit the CLI with an error code, BYPASSING
* error reporting through Sentry.io's platform (raven.Raven.captureException).
* Note that lib/errors.ts provides top-level error handling code to catch any
* otherwise uncaught errors, AND to report them through Sentry.io. But many
* "expected" errors (say, a JSON parsing error in a file provided by the user)
* don't warrant reporting through Sentry.io. For such mundane errors, catch
* 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) {
({ message } = message);
}
printErrorMessage(message);
process.exit(1);
}

View File

@ -14,32 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as _ from 'lodash';
import * as Mixpanel from 'mixpanel';
import * as packageJSON from '../package.json';
import { getBalenaSdk } from './utils/lazy';
const getMixpanel = _.once((balenaUrl: string) => {
return Mixpanel.init('balena-main', {
host: `api.${balenaUrl}`,
path: '/mixpanel',
protocol: 'https',
});
});
interface CachedUsername {
token: string;
username: string;
}
/**
* Mixpanel.com analytics tracking (information on balena CLI usage).
* Track balena CLI usage events (product improvement analytics).
*
* @param commandSignature A string like, for example:
* "push <applicationOrDevice>"
* That's literally so: "applicationOrDevice" is NOT replaced with the actual
* application ID or device ID. The purpose is to find out the most / least
* "push <fleetOrDevice>"
* That's literally so: "fleetOrDevice" is NOT replaced with the actual
* fleet ID or device ID. The purpose is to find out the most / least
* used command verbs, so we can focus our development effort where it is most
* beneficial to end users.
*
@ -60,11 +50,10 @@ export async function trackCommand(commandSignature: string) {
});
}
const settings = await import('balena-settings-client');
const balenaUrl = settings.get('balenaUrl') as string;
const username = await (async () => {
const getStorage = await import('balena-settings-storage');
const dataDirectory = settings.get('dataDirectory') as string;
const dataDirectory = settings.get<string>('dataDirectory');
const storage = getStorage({ dataDirectory });
let token;
try {
@ -94,8 +83,6 @@ export async function trackCommand(commandSignature: string) {
}
})();
const mixpanel = getMixpanel(balenaUrl);
if (!process.env.BALENARC_NO_SENTRY) {
Sentry!.configureScope((scope) => {
scope.setUser({
@ -109,16 +96,43 @@ export async function trackCommand(commandSignature: string) {
!process.env.BALENA_CLI_TEST_TYPE &&
!process.env.BALENARC_NO_ANALYTICS
) {
await mixpanel.track(`[CLI] ${commandSignature}`, {
distinct_id: username,
version: packageJSON.version,
node: process.version,
arch: process.arch,
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
platform: process.platform,
});
const balenaUrl = settings.get<string>('balenaUrl');
await sendEvent(balenaUrl, `[CLI] ${commandSignature}`, username);
}
} catch {
// ignore
}
}
/**
* Make the event tracking HTTPS request to balenaCloud's '/mixpanel' endpoint.
*/
async function sendEvent(balenaUrl: string, event: string, username?: string) {
const { default: got } = await import('got');
const trackData = {
event,
properties: {
arch: process.arch,
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
distinct_id: username,
mp_lib: 'node',
node: process.version,
platform: process.platform,
token: 'balena-main',
version: packageJSON.version,
},
};
const url = `https://api.${balenaUrl}/mixpanel/track`;
const searchParams = {
ip: 0,
verbose: 0,
data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
};
try {
await got(url, { searchParams, retry: 0 });
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] Event tracking error: ${e.message || e}`);
}
}
}

View File

@ -46,7 +46,7 @@ export default class BalenaHelp extends Help {
const subject = getHelpSubject(argv);
if (!subject) {
const verbose = argv.includes('-v') || argv.includes('--verbose');
this.showCustomRootHelp(verbose);
console.log(this.getCustomRootHelp(verbose));
return;
}
@ -80,69 +80,106 @@ export default class BalenaHelp extends Help {
throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`);
}
showCustomRootHelp(showAllCommands: boolean): void {
const chalk = getChalk();
const bold = chalk.bold;
const cmd = chalk.cyan.bold;
getCustomRootHelp(showAllCommands: boolean): string {
const { bold, cyan } = getChalk();
let commands = this.config.commands;
commands = commands.filter((c) => this.opts.all || !c.hidden);
// Get Primary Commands, sorted as in manual list
const primaryCommands = this.manuallySortedPrimaryCommands.map((pc) => {
return commands.find((c) => c.id === pc.replace(' ', ':'));
});
const primaryCommands = this.manuallySortedPrimaryCommands
.map((pc) => {
return commands.find((c) => c.id === pc.replace(' ', ':'));
})
.filter((c): c is typeof commands[0] => !!c);
// Get the rest as Additional Commands
const additionalCommands = commands.filter(
(c) =>
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
);
// Find longest usage, and pad usage of first command in each category
// This is to ensure that both categories align visually
const usageLength = commands
.map((c) => c.usage?.length || 0)
.reduce((longest, l) => {
return l > longest ? l : longest;
});
if (
typeof primaryCommands[0]?.usage === 'string' &&
typeof additionalCommands[0]?.usage === 'string'
) {
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
additionalCommands[0].usage = additionalCommands[0].usage.padEnd(
usageLength,
);
let usageLength = 0;
for (const cmd of primaryCommands) {
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
}
// Output help
console.log(bold('USAGE'));
console.log('$ balena [COMMAND] [OPTIONS]');
console.log(bold('\nPRIMARY COMMANDS'));
console.log(this.formatCommands(primaryCommands));
let additionalCmdSection: string[];
if (showAllCommands) {
console.log(bold('\nADDITIONAL COMMANDS'));
console.log(this.formatCommands(additionalCommands));
} else {
console.log(
`\n${bold('...MORE')} run ${cmd(
'balena help --verbose',
)} to list additional commands.`,
// Get the rest as Additional Commands
const additionalCommands = commands.filter(
(c) =>
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
);
// Find longest usage, and pad usage of first command in each category
// This is to ensure that both categories align visually
for (const cmd of additionalCommands) {
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
}
if (
typeof primaryCommands[0].usage === 'string' &&
typeof additionalCommands[0].usage === 'string'
) {
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
additionalCommands[0].usage =
additionalCommands[0].usage.padEnd(usageLength);
}
additionalCmdSection = [
bold('\nADDITIONAL COMMANDS'),
this.formatCommands(additionalCommands),
];
} else {
const cmd = cyan.bold('balena help --verbose');
additionalCmdSection = [
`\n${bold('...MORE')} run ${cmd} to list additional commands.`,
];
}
console.log(bold('\nGLOBAL OPTIONS'));
console.log(' --help, -h');
console.log(' --debug\n');
const globalOps = [
['--help, -h', 'display command help'],
['--debug', 'enable debug output'],
[
'--unsupported',
`\
prevent exit with an error as per Deprecation Policy
See: https://git.io/JRHUW#deprecation-policy`,
],
];
globalOps[0][0] = globalOps[0][0].padEnd(usageLength);
const {
const { deprecationPolicyNote, reachingOut } =
require('./utils/messages') as typeof import('./utils/messages');
return [
bold('USAGE'),
'$ balena [COMMAND] [OPTIONS]',
bold('\nPRIMARY COMMANDS'),
this.formatCommands(primaryCommands),
...additionalCmdSection,
bold('\nGLOBAL OPTIONS'),
this.formatGlobalOpts(globalOps),
bold('\nDeprecation Policy Reminder'),
deprecationPolicyNote,
reachingOut,
} = require('./utils/messages') as typeof import('./utils/messages');
console.log(reachingOut);
].join('\n');
}
protected formatGlobalOpts(opts: string[][]) {
const { dim } = getChalk();
const outLines: string[] = [];
let flagWidth = 0;
for (const opt of opts) {
flagWidth = Math.max(flagWidth, opt[0].length);
}
for (const opt of opts) {
const descriptionLines = opt[1].split('\n');
outLines.push(
` ${opt[0].padEnd(flagWidth + 2)}${dim(descriptionLines[0])}`,
);
outLines.push(
...descriptionLines
.slice(1)
.map((line) => ` ${' '.repeat(flagWidth + 2)}${dim(line)}`),
);
}
return outLines.join('\n');
}
protected formatCommands(commands: any[]): string {
@ -184,8 +221,8 @@ export default class BalenaHelp extends Help {
'push',
'logs',
'ssh',
'apps',
'app',
'fleets',
'fleet',
'devices',
'device',
'tunnel',

View File

@ -28,7 +28,7 @@ export const trackPromise = new Promise((resolve) => {
* parsed by oclif, but before the command's run() function is called.
* See: https://oclif.io/docs/hooks
*
* This hook is used to track CLI command signatures with mixpanel.
* This hook is used to track CLI command signatures (usage analytics).
* A command signature is something like "env add NAME [VALUE]". That's
* literally so: 'NAME' and 'VALUE' are NOT replaced with actual values.
*/

View File

@ -14,8 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { stripIndent } from './utils/lazy';
import { exitWithExpectedError } from './errors';
export let unsupportedFlag = false;
export interface AppOptions {
// Prevent the default behavior of flushing stdout after running a command
@ -50,11 +50,10 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
}
// support global --debug flag
const debugIndex = cmdSlice.indexOf('--debug');
if (debugIndex > -1) {
if (extractBooleanFlag(cmdSlice, '--debug')) {
process.env.DEBUG = '1';
cmdSlice.splice(debugIndex, 1);
}
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
}
// Enable bluebird long stack traces when in debug mode, must be set
@ -87,11 +86,22 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
return args;
}
function extractBooleanFlag(argv: string[], flag: string): boolean {
const index = argv.indexOf(flag);
if (index >= 0) {
argv.splice(index, 1);
return true;
}
return false;
}
/**
* Check whether the command line refers to a command that has been deprecated
* and removed and, if so, exit with an informative error message.
*/
export function checkDeletedCommand(argvSlice: string[]): void {
const { ExpectedError } = require('./errors') as typeof import('./errors');
if (argvSlice[0] === 'help') {
argvSlice = argvSlice.slice(1);
}
@ -101,17 +111,16 @@ export function checkDeletedCommand(argvSlice: string[]): void {
version: string,
verb = 'replaced',
) {
exitWithExpectedError(stripIndent`
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
Please use "balena ${alternative}" instead.
`);
throw new ExpectedError(`\
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
Please use "balena ${alternative}" instead.`);
}
function removed(oldCmd: string, alternative: string, version: string) {
let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`;
if (alternative) {
msg = [msg, alternative].join('\n');
}
exitWithExpectedError(msg);
throw new ExpectedError(msg);
}
const stopAlternative =
'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.';

View File

@ -25,7 +25,8 @@
export class CliSettings {
public readonly settings: any;
constructor() {
this.settings = require('balena-settings-client') as typeof import('balena-settings-client');
this.settings =
require('balena-settings-client') as typeof import('balena-settings-client');
}
public get<T>(name: string): T {

View File

@ -64,8 +64,8 @@ export const getDeviceAndAppFromUUID = _.memoize(
);
if (app == null) {
throw new ExpectedError(stripIndent`
Unable to access the application that device ${deviceUUID} belongs to.
Hint: check whether the application owner might have withdrawn access to it.
Unable to access the fleet that device ${deviceUUID} belongs to.
Hint: check whether the fleet owner withdrew access to it.
`);
}
return [device, app];
@ -139,6 +139,14 @@ export async function downloadOSImage(
const displayVersion = OSVersion === 'default' ? '' : ` ${OSVersion}`;
// Override the default zlib flush value as we've seen cases of
// incomplete files being identified as successful downloads when using Z_SYNC_FLUSH.
// Using Z_NO_FLUSH results in a Z_BUF_ERROR instead of a corrupt image file.
// https://github.com/nodejs/node/blob/master/doc/api/zlib.md#zlib-constants
// Hopefully this is a temporary workaround until we can resolve
// some ongoing issues with the os download stream.
process.env.ZLIB_FLUSH = 'Z_NO_FLUSH';
const manager = await import('balena-image-manager');
const stream = await manager.get(deviceType, OSVersion);
@ -188,20 +196,21 @@ async function resolveOSVersion(deviceType: string, version: string) {
return version;
}
const {
versions: vs,
recommended,
} = await getBalenaSdk().models.os.getSupportedVersions(deviceType);
const vs = (
(await getBalenaSdk().models.hostapp.getAllOsVersions([deviceType]))[
deviceType
] ?? []
).filter((v) => v.osType === 'default');
const choices = vs.map((v) => ({
value: v,
name: `v${v}` + (v === recommended ? ' (recommended)' : ''),
value: v.rawVersion,
name: `v${v.rawVersion}` + (v.isRecommended ? ' (recommended)' : ''),
}));
return getCliForm().ask({
message: 'Select the OS version:',
type: 'list',
choices,
default: recommended,
default: (vs.find((v) => v.isRecommended) ?? vs[0])?.rawVersion,
});
}

View File

@ -16,9 +16,9 @@
*/
import { lowercaseIfSlug } from './normalization';
export const applicationRequired = {
name: 'application',
description: 'application name, slug (preferred), or numeric ID (deprecated)',
export const fleetRequired = {
name: 'fleet',
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
required: true,
parse: lowercaseIfSlug,
};

View File

@ -20,15 +20,33 @@ import { flags } from '@oclif/command';
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
import { stripIndent } from './lazy';
import { lowercaseIfSlug } from './normalization';
import { isV13 } from './version';
export const v13: IBooleanFlag<boolean> = flags.boolean({
description: stripIndent`\
enable selected balena CLI v13 pre-release features, like the renaming
from "application" to "fleet" in command output`,
default: false,
});
export const application = flags.string({
char: 'a',
description: 'application name, slug (preferred), or numeric ID (deprecated)',
description: 'DEPRECATED alias for -f, --fleet',
parse: lowercaseIfSlug,
});
// TODO: Consider remove second alias 'app' when we can, to simplify.
export const app = flags.string({
description: "same as '--application'",
description: 'DEPRECATED alias for -f, --fleet',
parse: lowercaseIfSlug,
});
export const fleet = flags.string({
char: 'f',
description: isV13()
? 'fleet name, slug (preferred), or numeric ID (deprecated)'
: // avoid the '(deprecated)' remark in v12 while cf.application and
// cf.app are also described as deprecated, to avoid the impression
// that cf.fleet is deprecated as well.
'fleet name, slug (preferred), or numeric ID',
parse: lowercaseIfSlug,
});

View File

@ -51,7 +51,7 @@ export interface ComposeOpts {
dockerfilePath?: string;
inlineLogs?: boolean;
multiDockerignore: boolean;
nogitignore: boolean;
nogitignore: boolean; // v13: delete this line
noParentCheck: boolean;
projectName: string;
projectPath: string;
@ -63,9 +63,9 @@ export interface ComposeCliFlags {
dockerfile?: string;
logs: boolean;
nologs: boolean;
gitignore: boolean;
gitignore?: boolean; // v13: delete this line
nogitignore?: boolean; // v13: delete this line
'multi-dockerignore': boolean;
nogitignore: boolean;
'noparent-check': boolean;
'registry-secrets'?: RegistrySecrets;
'convert-eol': boolean;
@ -89,6 +89,9 @@ export interface Release {
| 'commit'
| 'composition'
| 'source'
| 'is_final'
| 'contract'
| 'semver'
| 'start_timestamp'
| 'end_timestamp'
>;
@ -99,6 +102,6 @@ interface TarDirectoryOptions {
composition?: Composition;
convertEol?: boolean;
multiDockerignore?: boolean;
nogitignore: boolean;
nogitignore: boolean; // v13: delete this line
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
}

View File

@ -18,6 +18,7 @@
import * as path from 'path';
import { ExpectedError } from '../errors';
import { getChalk } from './lazy';
import { isV13 } from './version';
/**
* @returns Promise<{import('./compose-types').ComposeOpts}>
@ -25,7 +26,7 @@ import { getChalk } from './lazy';
export function generateOpts(options) {
const { promises: fs } = require('fs');
if (options.gitignore && options['multi-dockerignore']) {
if (!isV13() && options.gitignore && options['multi-dockerignore']) {
throw new ExpectedError(
'The --gitignore and --multi-dockerignore options cannot be used together',
);
@ -37,7 +38,7 @@ export function generateOpts(options) {
convertEol: !options['noconvert-eol'],
dockerfilePath: options.dockerfile,
multiDockerignore: !!options['multi-dockerignore'],
nogitignore: !options.gitignore,
nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'],
}));
}
@ -48,10 +49,16 @@ export function generateOpts(options) {
/**
* @param {string} composePath
* @param {string} composeStr
* @param {string | null} projectName
* @param {string | undefined} projectName The --projectName flag (build, deploy)
* @param {string | undefined} imageTag The --tag flag (build, deploy)
* @returns {import('./compose-types').ComposeProject}
*/
export function createProject(composePath, composeStr, projectName = null) {
export function createProject(
composePath,
composeStr,
projectName = '',
imageTag = '',
) {
const yml = require('js-yaml');
const compose = require('resin-compose-parse');
@ -61,7 +68,7 @@ export function createProject(composePath, composeStr, projectName = null) {
});
const composition = compose.normalize(rawComposition);
projectName ??= path.basename(composePath);
projectName ||= path.basename(composePath);
const descriptors = compose.parse(composition).map(function (descr) {
// generate an image name based on the project and service names
@ -71,9 +78,8 @@ export function createProject(composePath, composeStr, projectName = null) {
descr.image.context != null &&
descr.image.tag == null
) {
descr.image.tag = [projectName, descr.serviceName]
.join('_')
.toLowerCase();
const { makeImageName } = require('./compose_ts');
descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag);
}
return descr;
});
@ -92,6 +98,8 @@ export function createProject(composePath, composeStr, projectName = null) {
* @param {string} dir Source directory
* @param {import('./compose-types').TarDirectoryOptions} param
* @returns {Promise<import('stream').Readable>}
*
* v13: delete this function
*/
export async function originalTarDirectory(dir, param) {
let {
@ -120,9 +128,10 @@ export async function originalTarDirectory(dir, param) {
}
const getFiles = () =>
// @ts-ignore `klaw` returns a `Walker` which is close enough to a stream to work but ts complains
Bluebird.resolve(streamToPromise(klaw(dir)))
// @ts-ignore
.filter((item) => !item.stats.isDirectory())
// @ts-ignore
.map((item) => item.path);
const ignore = new FileIgnorer(dir);
@ -174,6 +183,9 @@ export async function originalTarDirectory(dir, param) {
* @param {number} userId
* @param {number} appId
* @param {import('resin-compose-parse').Composition} composition
* @param {boolean} draft
* @param {string|undefined} semver
* @param {string|undefined} contract
* @returns {Promise<import('./compose-types').Release>}
*/
export const createRelease = async function (
@ -182,6 +194,9 @@ export const createRelease = async function (
userId,
appId,
composition,
draft,
semver,
contract,
) {
const _ = require('lodash');
const crypto = require('crypto');
@ -196,6 +211,9 @@ export const createRelease = async function (
composition,
source: 'local',
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
semver,
is_final: !draft,
contract,
});
return {
@ -206,6 +224,9 @@ export const createRelease = async function (
'commit',
'composition',
'source',
'is_final',
'contract',
'semver',
'start_timestamp',
'end_timestamp',
]),
@ -434,7 +455,6 @@ var pushProgressRenderer = function (tty, prefix) {
export class BuildProgressUI {
constructor(tty, descriptors) {
this._handleEvent = this._handleEvent.bind(this);
this._handleInterrupt = this._handleInterrupt.bind(this);
this.start = this.start.bind(this);
this.end = this.end.bind(this);
this._display = this._display.bind(this);
@ -486,14 +506,7 @@ export class BuildProgressUI {
this._serviceToDataMap[service] = event;
}
_handleInterrupt() {
this._cancelled = true;
this.end();
return process.exit(130); // 128 + SIGINT
}
start() {
process.on('SIGINT', this._handleInterrupt);
this._tty.hideCursor();
this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' });
@ -507,7 +520,6 @@ export class BuildProgressUI {
return;
}
this._ended = true;
process.removeListener('SIGINT', this._handleInterrupt);
this._runloop?.end();
this._runloop = null;

View File

@ -18,8 +18,9 @@ import { flags } from '@oclif/command';
import { BalenaSDK } from 'balena-sdk';
import type { TransposeOptions } from 'docker-qemu-transpose';
import type * as Dockerode from 'dockerode';
import * as _ from 'lodash';
import { promises as fs } from 'fs';
import jsyaml = require('js-yaml');
import * as _ from 'lodash';
import * as path from 'path';
import type {
BuildConfig,
@ -36,22 +37,26 @@ import {
ComposeCliFlags,
ComposeOpts,
ComposeProject,
Release,
TaggedImage,
TarDirectoryOptions,
} from './compose-types';
import type { DeviceInfo } from './device/api';
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
import Logger = require('./logger');
import { isV13 } from './version';
import { exists } from './which';
const allowedContractTypes = ['sw.application', 'sw.block'];
/**
* 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[] } {
export function parseReleaseTagKeysAndValues(releaseTags: string[]): {
releaseTagKeys: string[];
releaseTagValues: string[];
} {
if (releaseTags.length === 0) {
return { releaseTagKeys: [], releaseTagValues: [] };
}
@ -98,15 +103,6 @@ export async function applyReleaseTagKeysAndValues(
);
}
const exists = async (filename: string) => {
try {
await fs.access(filename);
return true;
} catch {
return false;
}
};
const LOG_LENGTH_MAX = 512 * 1024; // 512KB
const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml'];
const hr =
@ -122,6 +118,7 @@ export async function loadProject(
logger: Logger,
opts: ComposeOpts,
image?: string,
imageTag?: string,
): Promise<ComposeProject> {
const compose = await import('resin-compose-parse');
const { createProject } = await import('./compose');
@ -160,7 +157,12 @@ export async function loadProject(
}
}
logger.logDebug('Creating project...');
return createProject(opts.projectPath, composeStr, opts.projectName);
return createProject(
opts.projectPath,
composeStr,
opts.projectName,
imageTag,
);
}
/**
@ -242,7 +244,7 @@ interface Renderer {
streams: Dictionary<NodeJS.ReadWriteStream>;
}
export async function buildProject(opts: {
export interface BuildProjectOpts {
docker: Dockerode;
logger: Logger;
projectPath: string;
@ -255,40 +257,62 @@ export async function buildProject(opts: {
inlineLogs?: boolean;
convertEol: boolean;
dockerfilePath?: string;
nogitignore: boolean;
nogitignore: boolean; // v13: delete this line
multiDockerignore: boolean;
}): Promise<BuiltImage[]> {
}
export async function buildProject(
opts: BuildProjectOpts,
): Promise<BuiltImage[]> {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
const compose = await import('resin-compose-parse');
const imageDescriptors = compose.parse(opts.composition);
const renderer = await startRenderer({ imageDescriptors, ...opts });
let buildSummaryByService: Dictionary<string> | undefined;
try {
const { awaitInterruptibleTask } = await import('./helpers');
const [images, summaryMsgByService] = await awaitInterruptibleTask(
$buildProject,
imageDescriptors,
renderer,
opts,
);
buildSummaryByService = summaryMsgByService;
return images;
} finally {
renderer.end(buildSummaryByService);
}
}
async function $buildProject(
imageDescriptors: ImageDescriptor[],
renderer: Renderer,
opts: BuildProjectOpts,
): Promise<[BuiltImage[], Dictionary<string>]> {
const { logger, projectName } = opts;
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
let buildSummaryByService: Dictionary<string> | undefined;
const compose = await import('resin-compose-parse');
const imageDescriptors = compose.parse(opts.composition);
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
const tarStream = await tarDirectory(opts.projectPath, opts);
const tasks: BuildTaskPlus[] = await makeBuildTasks(
opts.composition,
tarStream,
opts,
logger,
projectName,
);
const imageDescriptorsByServiceName = _.keyBy(
imageDescriptors,
'serviceName',
);
const renderer = await startRenderer({ imageDescriptors, ...opts });
try {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
const tarStream = await tarDirectory(opts.projectPath, opts);
const tasks: BuildTaskPlus[] = await makeBuildTasks(
opts.composition,
tarStream,
opts,
logger,
projectName,
);
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
const transposeOptArray: Array<
TransposeOptions | undefined
> = await Promise.all(
const transposeOptArray: Array<TransposeOptions | undefined> =
await Promise.all(
tasks.map((task) => {
// Setup emulation if needed
if (needsQemu && !task.external) {
@ -297,41 +321,35 @@ export async function buildProject(opts: {
}),
);
await Promise.all(
// transposeOptions may be undefined. That's OK.
transposeOptArray.map((transposeOptions, index) =>
setTaskProgressHooks({
task: tasks[index],
renderer,
transposeOptions,
...opts,
}),
),
);
await Promise.all(
// transposeOptions may be undefined. That's OK.
transposeOptArray.map((transposeOptions, index) =>
setTaskProgressHooks({
task: tasks[index],
renderer,
transposeOptions,
...opts,
}),
),
);
logger.logDebug('Prepared tasks; building...');
logger.logDebug('Prepared tasks; building...');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('resin-multibuild');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('resin-multibuild');
const builtImages = await builder.performBuilds(
tasks,
opts.docker,
BALENA_ENGINE_TMP_PATH,
);
const builtImages = await builder.performBuilds(
tasks,
opts.docker,
BALENA_ENGINE_TMP_PATH,
);
const [images, summaryMsgByService] = await inspectBuiltImages({
builtImages,
imageDescriptorsByServiceName,
tasks,
...opts,
});
buildSummaryByService = summaryMsgByService;
return images;
} finally {
renderer.end(buildSummaryByService);
}
return await inspectBuiltImages({
builtImages,
imageDescriptorsByServiceName,
tasks,
...opts,
});
}
async function startRenderer({
@ -399,6 +417,18 @@ async function installQemuIfNeeded({
return needsQemu;
}
export function makeImageName(
projectName: string,
serviceName: string,
tag?: string,
) {
let name = `${projectName}_${serviceName}`;
if (tag) {
name = [name, tag].map((s) => s.replace(/:/g, '_')).join(':');
}
return name.toLowerCase();
}
function setTaskAttributes({
tasks,
buildOpts,
@ -414,7 +444,7 @@ function setTaskAttributes({
const d = imageDescriptorsByServiceName[task.serviceName];
// multibuild (splitBuildStream) parses the composition internally so
// any tags we've set before are lost; re-assign them here
task.tag ??= [projectName, task.serviceName].join('_').toLowerCase();
task.tag ??= makeImageName(projectName, task.serviceName, buildOpts.t);
if (isBuildConfig(d.image)) {
d.image.tag = task.tag;
}
@ -499,9 +529,8 @@ async function setTaskProgressHooks({
let rawStream;
stream = createLogStream(stream);
if (transposeOptions) {
const buildThroughStream = transpose.getBuildThroughStream(
transposeOptions,
);
const buildThroughStream =
transpose.getBuildThroughStream(transposeOptions);
rawStream = stream.pipe(buildThroughStream);
} else {
rawStream = stream;
@ -696,7 +725,7 @@ export async function getServiceDirsFromComposition(
* Return true if `image` is actually a docker-compose.yml `services.service.build`
* configuration object, rather than an "external image" (`services.service.image`).
*
* The `image` argument may therefore refere to either a `build` or `image` property
* The `image` argument may therefore refer to either a `build` or `image` property
* of a service in a docker-compose.yml file, which is a bit confusing but it matches
* the `ImageDescriptor.image` property as defined by `resin-compose-parse`.
*
@ -727,8 +756,8 @@ export async function tarDirectory(
dir: string,
param: TarDirectoryOptions,
): Promise<import('stream').Readable> {
const { nogitignore = false } = param;
if (nogitignore) {
const { nogitignore = false } = param; // v13: delete this line
if (isV13() || nogitignore) {
return newTarDirectory(dir, param);
} else {
return (await import('./compose')).originalTarDirectory(dir, param);
@ -749,11 +778,13 @@ async function newTarDirectory(
composition,
convertEol = false,
multiDockerignore = false,
nogitignore = false,
nogitignore = false, // v13: delete this line
preFinalizeCallback,
}: TarDirectoryOptions,
): Promise<import('stream').Readable> {
(await import('assert')).strict.equal(nogitignore, true);
if (!isV13()) {
require('assert').strict.equal(nogitignore, true);
}
const { filterFilesWithDockerignore } = await import('./ignore');
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
@ -767,10 +798,8 @@ async function newTarDirectory(
const tar = await import('tar-stream');
const pack = tar.pack();
const serviceDirs = await getServiceDirsFromComposition(dir, composition);
const {
filteredFileList,
dockerignoreFiles,
} = await filterFilesWithDockerignore(dir, multiDockerignore, serviceDirs);
const { filteredFileList, dockerignoreFiles } =
await filterFilesWithDockerignore(dir, multiDockerignore, serviceDirs);
printDockerignoreWarn(dockerignoreFiles, serviceDirs, multiDockerignore);
for (const fileStats of filteredFileList) {
pack.entry(
@ -836,16 +865,16 @@ function printDockerignoreWarn(
);
if (multiDockerignore) {
msg.push(stripIndent`
When --multi-dockerignore (-m) is used, only .dockerignore files at the root of
each service's build context (in a microservices/multicontainer application),
plus a .dockerignore file at the overall project root, are used.
When --multi-dockerignore (-m) is used, only .dockerignore files at the
root of each service's build context (in a microservices/multicontainer
fleet), plus a .dockerignore file at the overall project root, are used.
See "balena help ${Logger.command}" for more details.`);
} else {
msg.push(stripIndent`
By default, only one .dockerignore file at the source folder (project root)
is used. Microservices (multicontainer) applications may use a separate
.dockerignore file for each service with the --multi-dockerignore (-m) option.
See "balena help ${Logger.command}" for more details.`);
By default, only one .dockerignore file at the source folder (project
root) is used. Microservices (multicontainer) fleets may use a separate
.dockerignore file for each service with the --multi-dockerignore (-m)
option. See "balena help ${Logger.command}" for more details.`);
}
}
// No unused .dockerignore files. Print info-level advice in some cases.
@ -865,13 +894,14 @@ function printDockerignoreWarn(
msg.push(
stripIndent`
The --multi-dockerignore (-m) option was specified, but it has no effect for
single-container (non-microservices) apps. Only one .dockerignore file at the
single-container (non-microservices) fleets. Only one .dockerignore file at the
project source (root) directory, if any, is used. See "balena help ${Logger.command}".`,
);
}
}
if (msg.length) {
logFunc.call(logger, [' ', hr, ...msg, hr].join('\n'));
const { warnify } = require('./messages') as typeof import('./messages');
logFunc.call(logger, ' \n' + warnify(msg.join('\n'), ''));
}
}
@ -880,11 +910,16 @@ function printDockerignoreWarn(
* found and the --gitignore (-g) option has been provided (v11 compatibility).
* @param dockerignoreFile Absolute path to a .dockerignore file
* @param gitignoreFiles Array of absolute paths to .gitginore files
*
* v13: delete this function
*/
export function printGitignoreWarn(
dockerignoreFile: string,
gitignoreFiles: string[],
) {
if (isV13()) {
return;
}
const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter((e) => e);
if (ignoreFiles.length === 0) {
return;
@ -976,9 +1011,10 @@ async function parseRegistrySecrets(
}
const raw = (await fs.readFile(secretsFilename)).toString();
const multiBuild = await import('resin-multibuild');
const registrySecrets = new multiBuild.RegistrySecretValidator().validateRegistrySecrets(
isYaml ? require('js-yaml').load(raw) : JSON.parse(raw),
);
const registrySecrets =
new multiBuild.RegistrySecretValidator().validateRegistrySecrets(
isYaml ? require('js-yaml').load(raw) : JSON.parse(raw),
);
multiBuild.addCanonicalDockerHubEntry(registrySecrets);
return registrySecrets;
} catch (error) {
@ -1015,9 +1051,7 @@ export async function makeBuildTasks(
infoStr = `build [${task.context}]`;
}
logger.logDebug(` ${task.serviceName}: ${infoStr}`);
// Workaround for Docker v20.10 + single-arch base images. See:
// https://www.flowdock.com/app/rulemotion/i-cli/threads/RuSu1KiWOn62xaGy7O2sn8m8BUc
task.dockerPlatform = 'none';
task.logger = logger.getAdapter();
});
logger.logDebug(
@ -1074,18 +1108,21 @@ async function performResolution(
if (!buildTask.buildStream) {
continue;
}
// Consume each task.buildStream in order to trigger the
// resolution events that define fields like:
// task.dockerfile, task.dockerfilePath,
// task.projectType, task.resolved
// This mimics what is currently done in `resin-builder`.
const clonedStream: Pack = await cloneTarStream(
buildTask.buildStream,
);
buildTask.buildStream = clonedStream;
if (!buildTask.external && !buildTask.resolved) {
let error: Error | undefined;
try {
// Consume each task.buildStream in order to trigger the
// resolution events that define fields like:
// task.dockerfile, task.dockerfilePath,
// task.projectType, task.resolved
// This mimics what is currently done in `resin-builder`.
buildTask.buildStream = await cloneTarStream(buildTask.buildStream);
} catch (e) {
error = e;
}
if (error || (!buildTask.external && !buildTask.resolved)) {
const cause = error ? `${error}\n` : '';
throw new ExpectedError(
`Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
`${cause}Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
);
}
}
@ -1280,19 +1317,25 @@ async function pushServiceImages(
const { pushAndUpdateServiceImages } = await import('./compose');
const releaseMod = await import('balena-release');
logger.logInfo('Pushing images to registry...');
await pushAndUpdateServiceImages(docker, token, taggedImages, async function (
serviceImage,
) {
logger.logDebug(
`Saving image ${serviceImage.is_stored_at__image_location}`,
);
if (skipLogUpload) {
delete serviceImage.build_log;
}
await releaseMod.updateImage(pineClient, serviceImage.id, serviceImage);
});
await pushAndUpdateServiceImages(
docker,
token,
taggedImages,
async function (serviceImage) {
logger.logDebug(
`Saving image ${serviceImage.is_stored_at__image_location}`,
);
if (skipLogUpload) {
delete serviceImage.build_log;
}
await releaseMod.updateImage(pineClient, serviceImage.id, serviceImage);
},
);
}
// TODO: This should be shared between the CLI & the Builder
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
export async function deployProject(
docker: import('dockerode'),
logger: Logger,
@ -1303,6 +1346,8 @@ export async function deployProject(
auth: string,
apiEndpoint: string,
skipLogUpload: boolean,
projectPath: string,
isDraft: boolean,
): Promise<import('balena-release/build/models').ReleaseModel> {
const releaseMod = await import('balena-release');
const { createRelease, tagServiceImages } = await import('./compose');
@ -1310,40 +1355,56 @@ export async function deployProject(
const prefix = getChalk().cyan('[Info]') + ' ';
const spinner = createSpinner();
let runloop = runSpinner(tty, spinner, `${prefix}Creating release...`);
let $release: Release;
try {
$release = await createRelease(
apiEndpoint,
auth,
userId,
appId,
composition,
);
} finally {
runloop.end();
const contractPath = path.join(projectPath, 'balena.yml');
const contract = await getContractContent(contractPath);
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
throw new ExpectedError(stripIndent`\
Error: expected the version field in "${contractPath}"
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
}
const $release = await runSpinner(
tty,
spinner,
`${prefix}Creating release...`,
() =>
createRelease(
apiEndpoint,
auth,
userId,
appId,
composition,
isDraft,
contract?.version,
contract ? JSON.stringify(contract) : undefined,
),
);
const { client: pineClient, release, serviceImages } = $release;
try {
logger.logDebug('Tagging images...');
const taggedImages = await tagServiceImages(docker, images, serviceImages);
try {
const token = await getTokenForPreviousRepos(
logger,
appId,
apiEndpoint,
taggedImages,
);
await pushServiceImages(
docker,
logger,
pineClient,
taggedImages,
token,
skipLogUpload,
);
const { awaitInterruptibleTask } = await import('./helpers');
// awaitInterruptibleTask throws SIGINTError on CTRL-C,
// causing the release status to be set to 'failed'
await awaitInterruptibleTask(async () => {
const token = await getTokenForPreviousRepos(
logger,
appId,
apiEndpoint,
taggedImages,
);
await pushServiceImages(
docker,
logger,
pineClient,
taggedImages,
token,
skipLogUpload,
);
});
release.status = 'success';
} catch (err) {
release.status = 'failed';
@ -1355,15 +1416,12 @@ export async function deployProject(
);
}
} finally {
runloop = runSpinner(tty, spinner, `${prefix}Saving release...`);
release.end_timestamp = new Date();
if (release.id != null) {
try {
await runSpinner(tty, spinner, `${prefix}Saving release...`, async () => {
release.end_timestamp = new Date();
if (release.id != null) {
await releaseMod.updateRelease(pineClient, release.id, release);
} finally {
runloop.end();
}
}
});
}
return release;
}
@ -1374,21 +1432,26 @@ export function createSpinner() {
return () => chars[index++ % chars.length];
}
function runSpinner(
async function runSpinner<T>(
tty: ReturnType<typeof import('./tty')>,
spinner: () => string,
msg: string,
) {
fn: () => Promise<T>,
): Promise<T> {
const runloop = createRunLoop(function () {
tty.clearLine();
tty.writeLine(`${msg} ${spinner()}`);
return tty.cursorUp();
tty.cursorUp();
});
runloop.onEnd = function () {
tty.clearLine();
return tty.writeLine(msg);
tty.writeLine(msg);
};
return runloop;
try {
return await fn();
} finally {
runloop.end();
}
}
export function createRunLoop(tick: (...args: any[]) => void) {
@ -1405,6 +1468,42 @@ export function createRunLoop(tick: (...args: any[]) => void) {
return runloop;
}
async function getContractContent(
filePath: string,
): Promise<Dictionary<any> | undefined> {
let fileContentAsString;
try {
fileContentAsString = await fs.readFile(filePath, 'utf8');
} catch (e) {
if (e.code === 'ENOENT') {
return; // File does not exist
}
throw e;
}
let asJson;
try {
asJson = jsyaml.load(fileContentAsString);
} catch (err) {
throw new ExpectedError(
`Error parsing file "${filePath}":\n ${err.message}`,
);
}
if (!isContract(asJson)) {
throw new ExpectedError(
stripIndent`Error: application contract in '${filePath}' needs to
define a top level "type" field with an allowed application type.
Allowed application types are: ${allowedContractTypes.join(', ')}`,
);
}
return asJson;
}
function isContract(obj: any): obj is Dictionary<any> {
return obj?.type && allowedContractTypes.includes(obj.type);
}
function createLogStream(input: Readable) {
const split = require('split') as typeof import('split');
const stripAnsi = require('strip-ansi-stream');
@ -1545,22 +1644,26 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
description:
'Hide the image build log output (produce less verbose output)',
}),
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.`,
char: 'g',
}),
...(isV13()
? {}
: {
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.`,
char: 'g',
}),
nogitignore: flags.boolean({
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
char: 'G',
}),
}),
'multi-dockerignore': flags.boolean({
description:
'Have each service use its own .dockerignore file. See "balena help build".',
char: 'm',
}),
nogitignore: flags.boolean({
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
char: 'G',
}),
'noparent-check': flags.boolean({
description:
"Disable project validation check of 'docker-compose.yml' file in parent folder",
@ -1579,8 +1682,9 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).",
}),
projectName: flags.string({
description:
'Specify an alternate project name; default is the directory name',
description: stripIndent`\
Name prefix for locally built images. This is the 'projectName' portion
in 'projectName_serviceName:tag'. The default is the directory name.`,
char: 'n',
}),
};

View File

@ -20,6 +20,7 @@ import * as os from 'os';
import * as request from 'request';
import type * as Stream from 'stream';
import { retry } from '../helpers';
import Logger = require('../logger');
import * as ApiErrors from './errors';
@ -211,17 +212,21 @@ export class DeviceAPI {
);
return;
}
res.socket.setKeepAlive(true, 1000);
if (os.platform() !== 'win32') {
const NetKeepalive = await import('net-keepalive');
// Certain versions of typescript won't convert
// this automatically
const sock = (res.socket as any) as NodeJSSocketWithFileDescriptor;
// We send a tcp keepalive probe once every 5 seconds
NetKeepalive.setKeepAliveInterval(sock, 5000);
// After 5 failed probes, the connection is marked as
// closed
NetKeepalive.setKeepAliveProbes(sock, 5);
try {
res.socket.setKeepAlive(true, 1000);
if (os.platform() !== 'win32') {
const NetKeepalive = await import('net-keepalive');
// Certain versions of typescript won't convert
// this automatically
const sock = res.socket as any as NodeJSSocketWithFileDescriptor;
// We send a tcp keepalive probe once every 5 seconds
NetKeepalive.setKeepAliveInterval(sock, 5000);
// After 5 failed probes, the connection is marked as
// closed
NetKeepalive.setKeepAliveProbes(sock, 5);
}
} catch (error) {
reject(error);
}
resolve(res);
});
@ -235,7 +240,7 @@ export class DeviceAPI {
// A helper method for promisifying general (non-streaming) requests. Streaming
// requests should use a seperate setup
private static async promisifiedRequest<
T extends Parameters<typeof request>[0]
T extends Parameters<typeof request>[0],
>(opts: T, logger?: Logger): Promise<any> {
interface ObjectWithUrl {
url?: string;
@ -256,24 +261,35 @@ export class DeviceAPI {
}
}
return new Promise((resolve, reject) => {
return request(opts, (err, response, body) => {
if (err) {
return reject(err);
}
switch (response.statusCode) {
case 200:
return resolve(body);
case 400:
return reject(new ApiErrors.BadRequestDeviceAPIError(body.message));
case 503:
return reject(
new ApiErrors.ServiceUnavailableAPIError(body.message),
);
default:
return reject(new ApiErrors.DeviceAPIError(body.message));
}
const doRequest = async () => {
return await new Promise((resolve, reject) => {
return request(opts, (err, response, body) => {
if (err) {
return reject(err);
}
switch (response.statusCode) {
case 200:
return resolve(body);
case 400:
return reject(
new ApiErrors.BadRequestDeviceAPIError(body.message),
);
case 503:
return reject(
new ApiErrors.ServiceUnavailableAPIError(body.message),
);
default:
return reject(new ApiErrors.DeviceAPIError(body.message));
}
});
});
};
return await retry({
func: doRequest,
initialDelayMs: 2000,
maxAttempts: 6,
label: `Supervisor API (${opts.method} ${(opts as any).url})`,
});
}
}

View File

@ -57,7 +57,7 @@ export interface DeviceDeployOptions {
registrySecrets: RegistrySecrets;
multiDockerignore: boolean;
nocache: boolean;
nogitignore: boolean;
nogitignore: boolean; // v13: delete this line
noParentCheck: boolean;
nolive: boolean;
pull: boolean;
@ -182,7 +182,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
convertEol: opts.convertEol,
dockerfilePath: opts.dockerfilePath,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore,
nogitignore: opts.nogitignore, // v13: delete this line
noParentCheck: opts.noParentCheck,
projectName: 'local',
projectPath: opts.source,
@ -201,7 +201,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
composition: project.composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore,
nogitignore: opts.nogitignore, // v13: delete this line
});
// Try to detect the device information
@ -310,7 +310,7 @@ function connectToDocker(host: string, port: number): Docker {
});
}
export async function performBuilds(
async function performBuilds(
composition: Composition,
tarStream: Readable,
docker: Docker,
@ -426,7 +426,7 @@ export async function rebuildSingleTask(
composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore,
nogitignore: opts.nogitignore, // v13: delete this line
});
const task = _.find(

View File

@ -151,9 +151,8 @@ export class LivepushManager {
// only happens when the dockerfile path is
// specified differently - this should be patched
// in resin-bundle-resolve
this.dockerfilePaths[
buildTask.serviceName
] = this.getDockerfilePathFromTask(buildTask);
this.dockerfilePaths[buildTask.serviceName] =
this.getDockerfilePathFromTask(buildTask);
} else {
this.dockerfilePaths[buildTask.serviceName] = [
buildTask.dockerfilePath,

View File

@ -80,16 +80,19 @@ async function displayDeviceLogs(
jsonStream.on('error', (e) => {
logger.logWarn(`Error parsing NDJSON log chunk: ${e}`);
});
logs.once('error', reject);
logs.once('end', () => {
logs.once('error', handleError);
logs.once('end', handleError);
logs.pipe(jsonStream);
function handleError(error?: Error | string) {
logger.logWarn(DeviceConnectionLostError.defaultMsg);
if (gotSignal) {
reject(new SIGINTError('Log streaming aborted on SIGINT signal'));
} else {
reject(new DeviceConnectionLostError());
const msg = typeof error === 'string' ? error : error?.message;
reject(new DeviceConnectionLostError(msg));
}
});
logs.pipe(jsonStream);
}
});
} finally {
process.removeListener('SIGINT', handleSignal);

View File

@ -72,7 +72,9 @@ export const dockerConnectionCliFlags: flags.Input<DockerConnectionCliFlags> = {
export const dockerCliFlags: flags.Input<DockerCliFlags> = {
tag: flags.string({
description: 'The alias to the generated image',
description: `\
Tag locally built Docker images. This is the 'tag' portion
in 'projectName_serviceName:tag'. The default is 'latest'.`,
char: 't',
}),
buildArg: flags.string({
@ -105,7 +107,7 @@ export interface BuildOpts {
pull?: boolean;
registryconfig?: import('resin-multibuild').RegistrySecrets;
squash?: boolean;
t?: string;
t?: string; // only the tag portion of the image name, e.g. 'abc' in 'myimg:abc'
}
function parseBuildArgs(args: string[]): Dictionary<string> {

View File

@ -32,8 +32,7 @@ export const booleanConfig: IBooleanFlag<boolean> = flags.boolean({
export const booleanDevice: IBooleanFlag<boolean> = flags.boolean({
char: 'd',
description:
'select a device-specific variable instead of an application (fleet) variable',
description: 'select a device-specific variable instead of a fleet variable',
default: false,
});
@ -49,23 +48,23 @@ export const rmRenameHelp = stripIndent`
Variables are selected by their database ID (as reported by the 'balena envs'
command) and one of six database "resource types":
- application (fleet) environment variable
- application (fleet) configuration variable (--config)
- application (fleet) service variable (--service)
- fleet environment variable
- fleet configuration variable (--config)
- fleet service variable (--service)
- device environment variable (--device)
- device configuration variable (--device --config)
- device service variable (--device --service)
The --device option selects a device-specific variable instead of an application
(fleet) variable.
The --device option selects a device-specific variable instead of a fleet
variable.
The --config option selects a configuration variable. Configuration variable
names typically start with the 'BALENA_' or 'RESIN_' prefixes and are used to
configure balena platform features.
The --service option selects a service variable, which is an environment variable
that applies to a specifc service (application container) in a microservices
(multicontainer) application.
that applies to a specifc service (container) in a microservices (multicontainer)
fleet.
The --service and --config options cannot be used together, but they can be
used alongside the --device option to select a device-specific service or

View File

@ -90,9 +90,8 @@ export async function sudo(
}
export function runCommand<T>(commandArgs: string[]): Promise<T> {
const {
isSubcommand,
} = require('../preparser') as typeof import('../preparser');
const { isSubcommand } =
require('../preparser') as typeof import('../preparser');
if (isSubcommand(commandArgs)) {
commandArgs = [
commandArgs[0] + ':' + commandArgs[1],
@ -145,27 +144,22 @@ export async function osProgressHandler(step: InitializeEmitter) {
await new Promise((resolve, reject) => {
step.on('error', reject);
step.on('end', resolve);
step.on('end' as any, resolve);
});
}
export function getAppWithArch(
export async function getAppWithArch(
applicationName: string,
): Promise<ApplicationWithDeviceType & { arch: string }> {
return Promise.all([
getApplication(applicationName),
getBalenaSdk().models.config.getDeviceTypes(),
]).then(function ([app, deviceTypes]) {
const config = _.find<BalenaSdk.DeviceTypeJson.DeviceType>(deviceTypes, {
slug: app.is_for__device_type[0].slug,
});
const app = await getApplication(applicationName);
const { getExpanded } = await import('./pine');
if (!config) {
throw new Error('Could not read application information!');
}
return { ...app, arch: config.arch };
});
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
};
}
// TODO: Drop this. The sdk now has this baked in application.get().
@ -183,6 +177,11 @@ function getApplication(
},
is_for__device_type: {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
};
@ -406,90 +405,6 @@ function windowsCmdExeEscapeArg(arg: string): string {
return `"${arg.replace(/["]/g, '""')}"`;
}
/**
* Error handling wrapper around the npm `which` package:
* "Like the unix which utility. Finds the first instance of a specified
* executable in the PATH environment variable. Does not cache the results,
* so hash -r is not needed when the PATH changes."
*
* @param program Basename of a program, for example 'ssh'
* @param rejectOnMissing If the program cannot be found, reject the promise
* with an ExpectedError instead of fulfilling it with an empty string.
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
*/
export async function which(
program: string,
rejectOnMissing = true,
): Promise<string> {
const whichMod = await import('which');
let programPath: string;
try {
programPath = await whichMod(program);
} catch (err) {
if (err.code === 'ENOENT') {
if (rejectOnMissing) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
`'${program}' program not found. Is it installed?`,
);
} else {
return '';
}
}
throw err;
}
return programPath;
}
/**
* Call which(programName) and spawn() with the given arguments.
*
* If returnExitCodeOrSignal is true, the returned promise will resolve to
* an array [code, signal] with the child process exit code number or exit
* signal string respectively (as provided by the spawn close event).
*
* If returnExitCodeOrSignal is false, the returned promise will reject with
* a custom error if the child process returns a non-zero exit code or a
* non-empty signal string (as reported by the spawn close event).
*
* In either case and if spawn itself emits an error event or fails synchronously,
* the returned promise will reject with a custom error that includes the error
* message of spawn's error.
*/
export async function whichSpawn(
programName: string,
args: string[],
options: import('child_process').SpawnOptions = { stdio: 'inherit' },
returnExitCodeOrSignal = false,
): Promise<[number | undefined, string | undefined]> {
const { spawn } = await import('child_process');
const program = await which(programName);
if (process.env.DEBUG) {
console.error(`[debug] [${program}, ${args.join(', ')}]`);
}
let error: Error | undefined;
let exitCode: number | undefined;
let exitSignal: string | undefined;
try {
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
spawn(program, args, options)
.on('error', reject)
.on('close', (code, signal) => resolve([code, signal]));
});
} catch (err) {
error = err;
}
if (error || (!returnExitCodeOrSignal && (exitCode || exitSignal))) {
const msg = [
`${programName} failed with exit code=${exitCode} signal=${exitSignal}:`,
`[${program}, ${args.join(', ')}]`,
...(error ? [`${error}`] : []),
];
throw new Error(msg.join('\n'));
}
return [exitCode, exitSignal];
}
export interface ProxyConfig {
host: string;
port: string;
@ -548,13 +463,27 @@ export function getProxyConfig(): ProxyConfig | undefined {
}
}
export const expandForAppName: BalenaSdk.PineOptions<BalenaSdk.Device> = {
export const expandForAppName = {
$expand: {
belongs_to__application: { $select: 'app_name' },
is_of__device_type: { $select: 'slug' },
is_running__release: { $select: 'commit' },
},
};
} as const;
export const expandForAppNameAndCpuArch = {
$expand: {
...expandForAppName.$expand,
is_of__device_type: {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
} as const;
/**
* Use the `readline` library on Windows to install SIGINT handlers.
@ -598,14 +527,13 @@ export function addSIGINTHandler(sigintHandler: () => void, once = true) {
* @param theArgs Arguments to be passed to the task function
*/
export async function awaitInterruptibleTask<
T extends (...args: any[]) => Promise<any>
T extends (...args: any[]) => Promise<any>,
>(task: T, ...theArgs: Parameters<T>): Promise<ReturnType<T>> {
let sigintHandler: () => void = () => undefined;
const sigintPromise = new Promise<T>((_resolve, reject) => {
sigintHandler = () => {
const {
SIGINTError,
} = require('../errors') as typeof import('../errors');
const { SIGINTError } =
require('../errors') as typeof import('../errors');
reject(new SIGINTError('Task aborted on SIGINT signal'));
};
addSIGINTHandler(sigintHandler);
@ -617,15 +545,30 @@ export async function awaitInterruptibleTask<
}
}
/** Check if `drive` is mounted, and if so umount it. No-op on Windows. */
export async function safeUmount(drive: string) {
if (!drive) {
return;
}
const { isMounted, umount } = await import('umount');
const isMountedAsync = promisify(isMounted);
if (await isMountedAsync(drive)) {
const umountAsync = promisify(umount);
await umountAsync(drive);
}
/**
* Pick object fields like lodash _.pick(), but also interpret "renaming
* specifications" for object keys such as "appName => fleetName" as used
* by the 'resin-cli-visuals' package.
*
* Sample input: ({ 'a': 1, 'b': 2, 'c': 3 }, ['b => x', 'c', 'd'])
* Sample output: { 'x': 2, 'c': 3 }
*/
export function pickAndRename<T extends Dictionary<any>>(
obj: T,
fields: string[],
): Dictionary<any> {
const rename: Dictionary<any> = {};
// map 'a => b' to 'a' and setup rename['a'] = 'b'
fields = fields.map((f) => {
let renameFrom = f;
let renameTo = f;
const match = f.match(/(?<from>\S+)\s+=>\s+(?<to>\S+)/);
if (match && match.groups) {
renameFrom = match.groups.from;
renameTo = match.groups.to;
}
rename[renameFrom] = renameTo;
return renameFrom;
});
return _.mapKeys(_.pick(obj, fields), (_val, key) => rename[key]);
}

View File

@ -27,6 +27,7 @@ import { ExpectedError } from '../errors';
const { toPosixPath } = MultiBuild.PathUtils;
// v13: delete this enum
export enum IgnoreFileType {
DockerIgnore,
GitIgnore,
@ -42,6 +43,8 @@ interface IgnoreEntry {
* This class is used by the CLI v10 / v11 "original" tarDirectory function
* in `compose.js`. It is still around for the benefit of the `--gitignore`
* option, but is expected to be deleted in CLI v13.
*
* v13: delete this class
*/
export class FileIgnorer {
private dockerIgnoreEntries: IgnoreEntry[];

View File

@ -61,5 +61,6 @@ export const getCliForm = once(
export const getCliUx = once(() => require('cli-ux').ux as typeof ux);
// Directly export stripIndent as we always use it immediately, but importing just `stripIndent` reduces startup time
// tslint:disable-next-line:no-var-requires
export const stripIndent = require('common-tags/lib/stripIndent') as typeof StripIndent;
export const stripIndent =
// tslint:disable-next-line:no-var-requires
require('common-tags/lib/stripIndent') as typeof StripIndent;

View File

@ -30,6 +30,14 @@ enum Level {
LIVEPUSH = 'livepush',
}
interface LoggerAdapter {
debug: (msg: string) => void;
error: (msg: string) => void;
info: (msg: string) => void;
log: (msg: string) => void;
warn: (msg: string) => void;
}
/**
* General purpose logger class with support for log streams and colours.
* Call `Logger.getLogger()` to retrieve a global shared instance of this
@ -57,6 +65,8 @@ class Logger {
protected deferredLogMessages: Array<[string, Level]>;
protected adapter: LoggerAdapter;
protected constructor() {
const logger = new StreamLogger();
const chalk = getChalk();
@ -91,6 +101,14 @@ class Logger {
this.formatMessage = logger.formatWithPrefix.bind(logger);
this.deferredLogMessages = [];
this.adapter = {
debug: (msg: string) => this.logDebug(msg),
error: (msg: string) => this.logError(msg),
info: (msg: string) => this.logInfo(msg),
log: (msg: string) => this.logLogs(msg),
warn: (msg: string) => this.logWarn(msg),
};
}
protected static logger: Logger;
@ -151,6 +169,10 @@ class Logger {
});
this.deferredLogMessages = [];
}
public getAdapter(): LoggerAdapter {
return this.adapter;
}
}
export = Logger;

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