mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
126 Commits
test-caxa
...
update-res
Author | SHA1 | Date | |
---|---|---|---|
9b6fb62e4f | |||
cc60e86507 | |||
bd774e8553 | |||
c493c33e38 | |||
9487b33144 | |||
befdae1b90 | |||
08dfc945f3 | |||
8791c2f4e1 | |||
be306e6a20 | |||
6cfff72c59 | |||
adae718c2e | |||
132e1a63b2 | |||
a18e182ae4 | |||
e098cdca17 | |||
b42af74983 | |||
8bb211e441 | |||
ffccbfba12 | |||
56c1af50c0 | |||
8b9e3ccdc8 | |||
de95262f93 | |||
ed49938504 | |||
52ad0f6a57 | |||
7f6738c73c | |||
88fc3f7714 | |||
1afb29b923 | |||
09a4e8db2d | |||
6c81440428 | |||
3eca65ce0d | |||
6319b9dc13 | |||
290acaecbb | |||
305c9045f0 | |||
b701151769 | |||
e03bbb7275 | |||
3fd66c39ae | |||
b30075a18b | |||
a4fc95e99b | |||
63d8e5e6a3 | |||
6244af3464 | |||
8773927b3f | |||
29a3fd40a2 | |||
d6faf060e6 | |||
352fd197b7 | |||
afb6f938b7 | |||
d3adbcdba9 | |||
33fce1f24f | |||
ab90a5f150 | |||
a8b2212fed | |||
6bb8df30dd | |||
0327ed766d | |||
1009958340 | |||
5ce17ea70f | |||
9c821511b1 | |||
d793335287 | |||
dc59b7e4b0 | |||
370b844538 | |||
a8c2724929 | |||
09dd2dd354 | |||
f3ab41841a | |||
3dee30a0fe | |||
d34073f695 | |||
24fe6666e4 | |||
3fd5981085 | |||
08ee8643cb | |||
8db36ccec9 | |||
deb3e4c4ac | |||
a8ff21af69 | |||
4c54d6c171 | |||
83f213c007 | |||
d0cdc900a2 | |||
9937b91606 | |||
972c2470c5 | |||
7d568a928b | |||
2331e0a3e5 | |||
cb9b6be24b | |||
c2d3eee7cc | |||
d8b08f7272 | |||
819bdac354 | |||
318de8f017 | |||
2b0341e12a | |||
21f7463607 | |||
19fd3094d1 | |||
7c4974f4f5 | |||
3b56ed278e | |||
254ef1c8cf | |||
d11f49e0f8 | |||
48d7d0ef5e | |||
c7bbbc4159 | |||
d2fabcaf30 | |||
e137c2aed2 | |||
58704b08d3 | |||
485a9e944f | |||
1d7a50f007 | |||
64a44e7a5f | |||
c3406603db | |||
f1fa187a58 | |||
6cb2893750 | |||
216172ed4f | |||
3717d8cc0f | |||
8338e2e933 | |||
918c2e912d | |||
be0622ec80 | |||
07eef7bb49 | |||
0892caa155 | |||
fa4e8e7b55 | |||
e624726e44 | |||
f914fa2d8a | |||
c8f5542c8a | |||
a2cad7bf53 | |||
3a871a0003 | |||
e552e36f7b | |||
c325f1158e | |||
f79ccc0c95 | |||
1ec8d9a4ca | |||
427b0d9b41 | |||
cfd790a193 | |||
36f4c1312b | |||
fe7cbf4f74 | |||
4e8b8fe582 | |||
2986e6cea3 | |||
bb6b4b255a | |||
350c4abb96 | |||
fec96b41ee | |||
1dba5cc7c1 | |||
43c6fe672f | |||
486cae1aaa | |||
4d588e51a7 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @balena-io/balena-cli
|
10
.github/ISSUE_TEMPLATE.md
vendored
10
.github/ISSUE_TEMPLATE.md
vendored
@ -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
|
||||
|
@ -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
322
CHANGELOG.md
322
CHANGELOG.md
@ -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]
|
||||
|
@ -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'],`
|
||||
|
||||
|
@ -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**
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
20
README.md
20
README.md
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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',
|
||||
|
@ -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: [
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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}]`;
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
;;
|
||||
|
@ -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) )
|
||||
;;
|
||||
|
@ -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
|
||||
|
1383
doc/cli.markdown
1383
doc/cli.markdown
File diff suppressed because it is too large
Load Diff
46
lib/app.ts
46
lib/app.ts
@ -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));
|
||||
|
||||
|
@ -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));
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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'));
|
||||
|
@ -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'));
|
||||
|
@ -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'));
|
||||
|
@ -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 || {});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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: {
|
||||
|
@ -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?`,
|
||||
);
|
||||
|
@ -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({
|
||||
|
@ -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).
|
||||
|
@ -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}`);
|
||||
|
@ -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')),
|
||||
|
69
lib/commands/env/add.ts
vendored
69
lib/commands/env/add.ts
vendored
@ -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;
|
||||
|
4
lib/commands/env/rename.ts
vendored
4
lib/commands/env/rename.ts
vendored
@ -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')}
|
||||
|
6
lib/commands/env/rm.ts
vendored
6
lib/commands/env/rm.ts
vendored
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
20
lib/commands/fleet/index.ts
Normal file
20
lib/commands/fleet/index.ts
Normal 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;
|
20
lib/commands/fleet/purge.ts
Normal file
20
lib/commands/fleet/purge.ts
Normal 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;
|
20
lib/commands/fleet/rename.ts
Normal file
20
lib/commands/fleet/rename.ts
Normal 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;
|
20
lib/commands/fleet/restart.ts
Normal file
20
lib/commands/fleet/restart.ts
Normal 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
20
lib/commands/fleet/rm.ts
Normal 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
20
lib/commands/fleets.ts
Normal 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;
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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!');
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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`);
|
||||
}
|
||||
|
@ -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)' : ''));
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
86
lib/commands/release/finalize.ts
Normal file
86
lib/commands/release/finalize.ts
Normal 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`);
|
||||
}
|
||||
}
|
128
lib/commands/release/index.ts
Normal file
128
lib/commands/release/index.ts
Normal 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
86
lib/commands/releases.ts
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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({
|
||||
|
@ -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'
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
230
lib/deprecation.ts
Normal 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.`;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
141
lib/help.ts
141
lib/help.ts
@ -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',
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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`.';
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
11
lib/utils/compose-types.d.ts
vendored
11
lib/utils/compose-types.d.ts
vendored
@ -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>;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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',
|
||||
}),
|
||||
};
|
||||
|
@ -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})`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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[];
|
||||
|
@ -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;
|
||||
|
@ -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
Reference in New Issue
Block a user