Compare commits

...

313 Commits

Author SHA1 Message Date
ab1d8aa6ba (v14) Migrate tabular commands to new output framework
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2022-03-25 18:14:57 +01:00
d2330f9ed1 v13.1.13 2022-02-10 14:29:18 +02:00
cc19b00998 Merge pull request #2455 from balena-io/lucianbuzzo/drop-unused-fn-awaitdevice
Drop unused awaitDevice utility function
2022-02-10 12:27:25 +00:00
ed5ac75a10 v13.1.12 2022-02-09 09:24:17 +02:00
465b8a1b5e Merge pull request #2451 from balena-io/bump-preload-v12
Update balena-preload to v12
2022-02-09 07:22:04 +00:00
eccadbdcb9 Drop unused awaitDevice utility function
Change-type: patch
Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
2022-02-01 17:43:28 +00:00
31eb734af1 Update balena-preload to v12
Update balena-preload from 11.0.0 to 12.0.0

Change-type: patch
Changelog-entry: preload: Stop using the deprecated /device-types/v1 API endpoints
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-01-27 19:27:58 +02:00
fa7b59d64f v13.1.11 2022-01-20 01:55:52 +02:00
1e42bfa0d5 Merge pull request #2450 from balena-io/types-node-v12
chore: Update @types/node to v12.20.42
2022-01-19 23:53:08 +00:00
5464e550e7 chore: lib/auth/utils.ts: Replace deprecated url.resolve, use async/await
Change-type: patch
2022-01-19 22:48:46 +00:00
c0f27a663d chore: Update @types/node to v12.20.42
Change-type: patch
2022-01-19 22:48:46 +00:00
d1c61c62ab v13.1.10 2022-01-16 21:29:44 +02:00
a9691bff57 Merge pull request #2446 from balena-io/2445-min-node-version-12.8.0
Update docs and package.json re min Node.js supported version (12.8.0)
2022-01-16 19:27:33 +00:00
f5d09a43cd Update docs and package.json re min Node.js supported version (12.8.0)
Resolves: #2445
Change-type: patch
2022-01-16 18:44:45 +00:00
d11e547e11 v13.1.9 2022-01-14 03:00:53 +02:00
bd462aee02 Merge pull request #2443 from balena-io/colors-action
Update packages in response to colors package issues
2022-01-14 00:58:59 +00:00
f633c0468b Update packages in response to colors package issues
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2022-01-12 22:12:06 +01:00
e4f61a1242 v13.1.8 2022-01-11 03:28:31 +02:00
96142a002e Merge pull request #2442 from balena-io/2440-pin-docker-modem
local push: Fix "invalid character '/' looking for beginning of value"
2022-01-11 01:26:51 +00:00
6b9a5cd89c local push: Fix "invalid character '/' looking for beginning of value"
Change-type: patch
2022-01-11 00:15:10 +00:00
ba2d3d60ec Merge pull request #2441 from balena-io/v14-TypeError-type-undefined
v14 preparations: Fix TypeError produced by 'npx oclif manifest'
2022-01-08 01:55:40 +00:00
d1e66bc1a5 v14 preparations: Fix TypeError produced by 'npx oclif manifest'
Change-type: patch
2022-01-08 01:16:45 +00:00
58799915a9 v13.1.7 2022-01-06 19:29:35 +02:00
5f2d55f569 Merge pull request #2436 from balena-io/update-pkg
Update to pkg 5
2022-01-06 17:27:47 +00:00
8d6e51391c v13.1.6 2022-01-04 18:53:40 +02:00
8454b02988 Merge pull request #2435 from balena-io/enforce-js-types
Automation: enforce noImplicitAny for the type-checked javascript
2022-01-04 16:51:05 +00:00
879d98ef98 Update to pkg 5
Change-type: patch
2022-01-04 16:31:08 +00:00
c4e317a290 Automation: enforce noImplicitAny for the type-checked javascript
Change-type: patch
2022-01-04 16:27:06 +00:00
7ca4d2d720 v13.1.5 2022-01-04 17:34:55 +02:00
e1e88ec56d Merge pull request #2434 from balena-io/remove-gulp
Build: switch from using inline-source via gulp to using it directly
2022-01-04 15:33:01 +00:00
33f7fa3829 Build: switch from using inline-source via gulp to using it directly
Change-type: patch
2022-01-04 15:03:05 +00:00
3d516e7c5f v13.1.4 2022-01-04 13:25:57 +02:00
a8507508b7 Merge pull request #2428 from balena-io/update-pkg
Update pkg
2022-01-04 11:24:14 +00:00
008972b3d3 Update pkg
Change-type: patch
2022-01-03 17:35:13 +00:00
92b86330a0 v13.1.3 2022-01-03 18:52:42 +02:00
2563c07c6a Merge pull request #2433 from balena-io/ts-deploy-legacy
Convert lib/utils/deploy-legacy to typescript
2022-01-03 16:50:06 +00:00
1d4b949cf3 Convert lib/utils/deploy-legacy to typescript
Change-type: patch
2022-01-03 16:10:17 +00:00
d17e02a930 v13.1.2 2022-01-03 18:03:55 +02:00
a355cbaa79 Merge pull request #2432 from balena-io/compose-ts
Convert lib/utils/compose to typescript
2022-01-03 16:01:18 +00:00
bd021c0a2d Convert lib/utils/compose to typescript
Change-type: patch
2022-01-03 15:26:19 +00:00
a80f676804 v13.1.1 2021-12-30 15:08:14 +02:00
f723c58089 Merge pull request #2430 from balena-io/update-deps
Update dependencies
2021-12-30 13:05:58 +00:00
e27a4e2e31 Update dependencies
Update docker-progress from 5.0.0 to 5.0.1

Change-type: patch
2021-12-30 12:36:12 +00:00
b91b72c408 v13.1.0 2021-12-29 16:47:51 +02:00
5cf84d3f1d Merge pull request #2431 from balena-io/os-configure-dev-flag
os.getConfig MVP (os configure, config generate, local configure)
2021-12-29 14:45:46 +00:00
7d58b8c120 os configure, config generate: Add '--dev' option for OS developmentMode
Change-type: minor
2021-12-29 00:28:04 +00:00
851301a336 local configure: Allow configuring 'developmentMode' in config.json
Change-type: minor
2021-12-25 02:26:52 +00:00
ec6fd050f6 os build-config: Clarify command purpose in help output
Change-type: patch
2021-12-25 02:26:47 +00:00
6f81053882 device os-update: Add support for unified dev/prod balenaOS releases
Update balena-sdk from 16.8.1 to 16.9.0

Change-type: minor
2021-12-24 23:52:57 +00:00
dbd8a9a08c v13.0.2 2021-12-24 20:12:14 +02:00
256f1abf1b Merge pull request #2427 from balena-io/update-oclif
Update oclif
2021-12-24 18:10:10 +00:00
acd352cb3c Update oclif
Change-type: patch
2021-12-24 17:20:50 +00:00
31f927c27c v13.0.1 2021-12-24 18:58:28 +02:00
3d0f16168a Merge pull request #2429 from balena-io/os-versions-recommended
os versions, os download: Replace deprecated version fields
2021-12-24 16:55:58 +00:00
b2d932afab os versions, os download: Replace deprecated version fields
Replace deprecated `rawVersion` and `formattedVersion` fields and use
alternative overload of `getAvailableOsVersions`. As a result, the word
'recommended' is no longer printed next to any OS versions.

Change-type: patch
2021-12-24 16:01:51 +00:00
398175f0b3 Update balena-sdk to v16.8.1
Update balena-sdk from 16.8.0 to 16.8.1

Change-type: patch
2021-12-24 14:45:12 +00:00
2fb9c6c773 v13.0.0 2021-12-23 21:48:02 +02:00
66608b32e9 Merge pull request #2420 from balena-io/v13
Release CLI v13
2021-12-23 19:45:50 +00:00
c403683edf v13 RELEASE NOTES: see https://git.io/JDHxG
Change-type: patch
2021-12-23 18:47:34 +00:00
1e6ab46ca3 Add tips for removed commands
Signed-off-by: Scott Lowe <scott@balena.io>
2021-12-23 18:40:05 +00:00
02d3220f2d Fix some app/fleet terminology issues
Signed-off-by: Scott Lowe <scott@balena.io>
2021-12-23 18:40:05 +00:00
c86cdc8f84 balena SDK v16: Ensure all SDK calls use fleet slug rather than name
Change-type: patch
2021-12-23 18:40:05 +00:00
84f02dc063 Update balena-sdk to v16.8.0
Update balena-sdk from 15.51.1 to 16.8.0

Change-type: patch
2021-12-23 18:40:05 +00:00
9145f2fb28 device, devices: Print the fleet's slug in 'org/fleetName' format
Change-type: major
2021-12-23 15:34:09 +00:00
1164388d78 envs: Print the fleet's slug in 'org/fleetName' format
Change-type: major
2021-12-23 15:34:08 +00:00
06f6094401 os configure: Remove deprecated '--device-api-key' option
Change-type: major
2021-12-23 15:34:08 +00:00
67e11467f7 Clean up unused v13 feature switch code
Change-type: patch
2021-12-23 15:34:08 +00:00
c8dfd0ca65 config read/write/inject/reconfigure: Place '--type' option behind v14 switch
Change-type: patch
2021-12-23 15:34:08 +00:00
8b110a835a fleet create: Don't print fleet's numeric database ID in confirmation msg
Change-type: major
2021-12-23 15:34:08 +00:00
7564d95f82 devices supported: Remove deprecated '--verbose' and '--discontinued' options
Change-type: major
2021-12-23 15:34:08 +00:00
f12f2b79ef build/deploy/push: Remove deprecated '--convert-eol' option
Change-type: major
2021-12-23 15:34:08 +00:00
176d731f9e Move some v13 features behind v14 switch.
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-12-23 15:34:08 +00:00
1ed39d1d37 Remove deprecated '--app' and '--application' options (renamed to '--fleet')
Change-type: major
2021-12-23 15:34:08 +00:00
580ca0d584 Remove deprecated commands 'app' and 'apps' (renamed to 'fleet' and 'fleets')
Change-type: major
2021-12-23 15:34:08 +00:00
73572df7cf build/deploy/push: Remove deprecated '--[no]gitignore' option
Change-type: major
2021-12-23 15:34:08 +00:00
23b42b1a2b v13 release: Flip the v13 feature switch
Change-type: major
2021-12-23 15:34:08 +00:00
632322e3c2 v13 release: Drop support for Node.js v10 (package.json engines.node)
Change-type: major
2021-12-23 15:34:08 +00:00
4faa5d7f57 v12.55.11 2021-12-23 17:19:56 +02:00
9b967592a9 Merge pull request #2426 from balena-io/typescript-4.5
Update to typescript 4.5
2021-12-23 15:18:13 +00:00
e01483cd2b Update to typescript 4.5
Change-type: patch
2021-12-23 13:59:42 +00:00
6d89ff4bbf v12.55.10 2021-12-23 15:04:02 +02:00
126e731117 Merge pull request #2423 from balena-io/update-dev-deps
Update dev dependencies
2021-12-23 13:01:42 +00:00
32d26ad074 v12.55.9 2021-12-22 18:02:30 +02:00
2bcfec9d0f Merge pull request #2413 from balena-io/os-download-prod-suffix-revisited
os download: Future-proof unified dev/prod balenaOS versioning
2021-12-22 16:00:49 +00:00
c04e63ab7d os download: Future-proof '--version' format for unified dev/prod variants
Do not append the '.prod' suffix by default to balenaOS versions.

Change-type: patch
2021-12-22 15:37:24 +00:00
79be06820c Update dev dependencies
Change-type: patch
2021-12-21 18:24:26 +00:00
ffb94c380f v12.55.8 2021-12-21 18:59:32 +02:00
385b5e9ec6 Merge pull request #2422 from balena-io/windows-version-info
Include version info when installed on windows
2021-12-21 16:55:54 +00:00
8d3a4343cb Include version info when installed on windows
Change-type: patch
2021-12-21 16:22:21 +00:00
6eeb16245b Switch from the deprecated oclif-dev commands to the oclif commands
Change-type: patch
2021-12-21 16:21:58 +00:00
3961060f90 v12.55.7 2021-12-14 23:32:04 +02:00
a6dfc9126a Merge pull request #2412 from balena-io/pp/issue-2411
Removed hardcoded 'balenaCloud' in console message.
2021-12-14 21:30:06 +00:00
e7ddd07b7b push: Remove hardcoded 'balenaCloud' in console message
Change-type: patch
Signed-off-by: Pranav Peshwe <pranav@balena.io>
2021-12-14 19:54:30 +05:30
fea351d960 v12.55.6 2021-12-14 03:57:53 +02:00
40e0b2dbed Merge pull request #2410 from balena-io/fix-symlink-push-deploy
Fix symbolic link regression in push & deploy
2021-12-14 01:56:06 +00:00
3def4d0e4a Fix symbolic link regression in push & deploy
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-12-14 02:30:08 +02:00
aa286cc0e7 v12.55.5 2021-12-14 01:58:31 +02:00
8abeb6aed7 Merge pull request #2409 from balena-io/drop-unused-dir-traversal-list
Drop unnecessary directory list created during balena deploy & push
2021-12-13 23:56:00 +00:00
f285880135 Drop unnecessary directory list created during balena deploy & push
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-12-14 00:29:15 +02:00
2b5c387313 v12.55.4 2021-12-10 03:29:22 +02:00
8babf4c908 Merge pull request #2405 from balena-io/2386-os-download-dt-aliases
os download, os versions: Accept device type aliases
2021-12-10 01:26:38 +00:00
bfc995e948 os download, os versions: Accept device type aliases
Change-type: patch
2021-12-10 00:52:02 +00:00
c6a0bc0fba os download: Don't append '.prod' if the OS version does not match regex 2021-12-10 00:38:56 +00:00
ae69accf0f v12.55.3 2021-12-10 00:01:36 +02:00
cfcace4c99 Merge pull request #2404 from balena-io/2387-os-download-prod-suffix
os download: Assume '.prod' suffix by default for all balenaOS versions
2021-12-09 21:59:27 +00:00
6e07db0813 os download: Improve error message when not logged in (balenaOS ESR versions)
Change-type: patch
2021-12-09 18:06:26 +00:00
5c40c8d51f os download: Assume '.prod' suffix by default for all balenaOS versions
Resolves: #2387
Change-type: patch
2021-12-09 17:56:16 +00:00
d827005154 v12.55.2 2021-12-08 14:05:10 +02:00
76081343cc Merge pull request #2147 from balena-io/rework-tables
v13 preparations: Standardize command data output
2021-12-08 12:02:36 +00:00
f3fb9b6bdf v13 preparations: Standardize command data output
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-12-08 12:10:08 +01:00
c125e0b38d v12.55.1 2021-12-01 01:49:43 +02:00
73b2f6b4b1 Merge pull request #2401 from balena-io/klutchell-patch-2
chore: Bump multicast-dns to rebased commit
2021-11-30 23:46:39 +00:00
fdc0d08e96 chore: Bump multicast-dns to rebased commit
Otherwise npm install fails due to the missing commit in npm-shrinkwrap.json

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
See: https://github.com/balena-io-modules/multicast-dns/pull/1
2021-11-30 11:07:39 -05:00
e431a59af7 v12.55.0 2021-11-30 00:12:46 +02:00
41a2dbe60c Merge pull request #2294 from balena-io/provisioning-names-keys
Add provisioning key name option to config generate options
2021-11-29 22:11:05 +00:00
6ba67eefdb Add provisioning key name option to config generate options
Change-Type: minor
Signed-off-by: Nitish Agarwal 1592163+nitishagar@users.noreply.github.com
2021-11-29 16:15:55 +05:30
3b885ad906 v12.54.5 2021-11-27 03:45:02 +02:00
5574dc0318 Merge pull request #2398 from balena-io/reuse-getbootpartition
os configure, local configure: Reuse disk partition scanning logic
2021-11-27 01:42:53 +00:00
fcea91bfb6 os configure, local configure: Reuse disk partition scanning logic
Change-type: patch
2021-11-27 01:10:53 +00:00
7316c4e075 v12.54.4 2021-11-26 18:39:20 +02:00
389b7a1463 Merge pull request #2389 from balena-io/balena-lint-no-floating-promises
Bump 'balena-lint' and fix 'no-floating-promises' warnings
2021-11-26 16:36:16 +00:00
09d004423c Bump 'balena-lint' and fix 'no-floating-promises' warnings
Change-type: patch
2021-11-26 15:59:33 +00:00
97978ff812 v12.54.3 2021-11-26 17:38:17 +02:00
498e21f0ab Merge pull request #2376 from balena-io/lucianbuzzo/fast-scan
Improve directory scan speed prior to tarballing
2021-11-26 15:35:56 +00:00
257dd514ed Improve directory scan speed prior to tarballing
This changes improves the speed that the project is tarballed by switching from
`klaw` to `recursive-fs` and not running `lstat` on files that are ignored.
Whilst testing with the Jellyfish repository, which contains a number of
sub directories, each with their own node_modules folder, I was able to
reduce the time taken to scan and tarball the project from 70s to 11s,
which is a massive improvement.

Change-type: patch
Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
2021-11-26 13:55:41 +00:00
85cbdd4947 v12.54.2 2021-11-26 14:01:22 +02:00
73625611da Merge pull request #2395 from balena-io/lucianbuzzo/2394-push-image
Set the correct target state when using Compose "image" field
2021-11-26 11:59:17 +00:00
d2a5a9ba86 Set the correct target state when using Compose "image" field
Fixes #2394

When pushing to a device in local mode, if a service is not external, and uses
an `image` field, that value should be used for tags and target state, otherwise
it won't match the image name generated on the device by balenaEngine.

Change-type: patch
Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
2021-11-26 10:11:07 +00:00
1cd78215e0 v12.54.1 2021-11-26 00:59:24 +02:00
6d744d0b07 Merge pull request #2393 from balena-io/fix-config-usage
Fix mistake in `config generate` examples
2021-11-25 22:56:40 +00:00
9d312bcd12 v12.54.0 2021-11-25 23:47:55 +02:00
e22aa847e3 Merge pull request #2378 from balena-io/events-timeout
Improve UX for offline usage
2021-11-25 21:45:47 +00:00
0d1ca67d5b v12.53.2 2021-11-25 20:58:34 +02:00
c4a5a25f03 Merge pull request #2391 from balena-io/drop-custom-device-api-key-generation-code
Stop creating an extra provisioning API key in each config generation
2021-11-25 18:56:53 +00:00
b183d88400 Fix mistake in config generate examples
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-11-25 16:05:37 +00:00
2b6a2142eb Improve UX for offline usage
Change-type: minor
Resolves: #2372
Signed-off-by: Scott Lowe <scott@balena.io>
2021-11-25 15:14:39 +00:00
58b29bf4bb Stop creating an extra provisioning API key in each config generation
Change-type: patch
Changelog-entry: Avoid creating an extra provisioning API key in os configure & config generate
See: https://github.com/balena-io/balena-cli/pull/2294#discussion_r756499196
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-11-25 16:40:53 +02:00
fc0903a414 v12.53.1 2021-11-25 12:18:50 +02:00
cea23f5d5e Merge pull request #2388 from balena-io/docs-changes
Transitional changes to doc files for landr implementation
2021-11-25 10:15:37 +00:00
5a9b5e3b08 Transitional changes to doc files for landr implementation
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-11-25 10:44:38 +01:00
52138d41eb v12.53.0 2021-11-25 04:49:54 +02:00
5acdc63068 Merge pull request #2369 from balena-io/2349-config-inject-scan-partition
config read/write/inject: Avoid need for internet access
2021-11-25 02:47:56 +00:00
b546e4dd97 config read/write/inject: Avoid need for internet access
Change-type: minor
2021-11-25 02:05:40 +00:00
e4870916e2 config read: Add '--json' option for JSON output
Change-type: minor
2021-11-24 23:03:37 +00:00
3ca93448cd v12.52.2 2021-11-24 20:59:10 +02:00
f66395e2d5 Merge pull request #2390 from balena-io/device-init-docs
Delete 'doc/automated-init.md' and improve 'balena help device init'
2021-11-24 18:57:11 +00:00
952d782e90 Delete 'doc/automated-init.md' and improve 'balena help device init'
Change-type: patch
2021-11-24 18:24:14 +00:00
d53c9b3c50 v12.52.1 2021-11-22 04:25:10 +02:00
2f706c0200 Merge pull request #2384 from balena-io/2376-dockerignore-corner-cases
push/build: Add test cases for .dockerignore filtering corner cases
2021-11-22 02:23:22 +00:00
d64b6deb81 push/build: Add test cases for .dockerignore filtering corner cases
Change-type: patch
2021-11-22 01:50:27 +00:00
55fc9b2ade v12.52.0 2021-11-20 03:19:00 +02:00
6c29d0ae27 Merge pull request #2334 from balena-io/2005-os-esr-versions-hostapp
os versions, os download: Add support for balenaOS ESR versions
2021-11-20 01:17:17 +00:00
f46452f6de os download: Display OS version actually downloaded (range or 'recommended')
Change-type: patch
2021-11-20 00:43:15 +00:00
c166ec7597 os versions, os download: Add support for balenaOS ESR versions
Change-type: minor
2021-11-20 00:43:15 +00:00
7325c79888 v12.51.3 2021-11-16 19:10:36 +02:00
2a29b386eb Merge pull request #2375 from balena-io/missing-digest
deploy: Ensure the release fails if an image's digest (hash) is missing
2021-11-16 17:08:41 +00:00
23b07f8a41 deploy: Ensure the release fails if an image's digest (hash) is missing
Change-type: patch
2021-11-16 11:55:07 +00:00
6d641b4841 v12.51.2 2021-11-16 13:50:52 +02:00
7b498149b1 Merge pull request #2379 from balena-io/remove-node10-from-resinci.yml
Update balena CI configuration (remove Node v10 from npm pipeline list)
2021-11-16 11:49:36 +00:00
ae5ea0f4e8 Update balena CI configuration (remove Node v10 from npm pipeline list)
Change-type: patch
2021-11-15 23:51:15 +00:00
f635f648da v12.51.1 2021-10-25 20:54:12 +03:00
3d4e2cf823 Merge pull request #2256 from balena-io/forum-link
Fix forums support link in README.md
2021-10-25 17:52:15 +00:00
ef3b630887 v12.51.0 2021-10-22 22:13:44 +03:00
19040ccb6c Merge pull request #2367 from balena-io/support-for-fragments
Add support for YAML anchors and aliases in 'docker-compose.yml'
2021-10-22 19:11:47 +00:00
8e712ac910 Add support for YAML anchors and aliases in 'docker-compose.yml'
This allows project files to define services from generic fragments by leveraging YAML's anchors and aliases. See here for an example: 43f6537b2c/spec.md (fragments)

Removing the FAILSAFE_SCHEMA flag is not expected to break existing project files, since the default behaviour is more liberal, or cause problems down the road given we perform validation immediately after. Docs for the flag: https://github.com/nodeca/js-yaml#load-string---options-

Change-type: minor
2021-10-22 16:42:29 +03:00
c401ed35ac v12.50.3 2021-10-20 19:33:37 +03:00
94be97313b Merge pull request #2359 from balena-io/klutchell/preload-11
preload: Avoid possible ValueError when parsing storage driver
2021-10-20 16:31:34 +00:00
48053ecefc preload: Avoid possible ValueError when parsing storage driver
Update balena-preload from 10.5.0 to 11.0.0

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

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

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

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

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

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

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

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

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

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

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

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

Change-type: patch
2021-07-21 23:56:57 +01:00
c8f5542c8a chore: Update oclif
Change-type: patch
2021-07-21 23:56:46 +01:00
a2cad7bf53 chore: Update dependencies ("npm audit fix")
Change-type: patch
2021-07-21 23:56:35 +01:00
3a871a0003 chore: Update balena-lint
Change-type: patch
2021-07-20 18:02:16 +01:00
e552e36f7b v12.44.25 2021-07-20 18:56:54 +03:00
c325f1158e Merge pull request #2295 from balena-io/simplify-run-spinner
Simplify runSpinner api
2021-07-20 15:55:11 +00:00
f79ccc0c95 Simplify runSpinner api
Change-type: patch
2021-07-20 15:46:54 +01:00
1ec8d9a4ca v12.44.24 2021-07-10 00:30:25 +03:00
427b0d9b41 Merge pull request #2291 from balena-io/fix-config-write-101
config write: Fix parsing of 'key' argument with numeric components
2021-07-09 21:28:19 +00:00
cfd790a193 Update 'devDependencies' in package.json
Change-type: patch
2021-07-09 21:34:09 +01:00
36f4c1312b config write: Fix parsing of 'key' argument with numeric components
Change-type: patch
2021-07-09 17:29:21 +01:00
fe7cbf4f74 v12.44.23 2021-06-30 19:26:18 +03:00
4e8b8fe582 Merge pull request #2287 from balena-io/dfunckt-patch-1
Delete CODEOWNERS
2021-06-30 16:23:38 +00:00
2986e6cea3 Delete CODEOWNERS
Change-type: patch
2021-06-30 18:55:44 +03:00
bb6b4b255a v12.44.22 2021-06-24 19:55:41 +03:00
350c4abb96 Merge pull request #2283 from balena-io/2045-sfdisk-spinner
preload: Catch sfdisk errors that result in an endless spinner
2021-06-24 16:53:35 +00:00
fec96b41ee preload: Warn that zip files are only accepted for Intel Edison
Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-06-24 11:04:26 -04:00
1dba5cc7c1 preload: Catch sfdisk errors that result in an endless spinner
Change-type: patch
Changelog-entry: preload: Catch sfdisk errors that result in an endless spinner
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-06-23 12:56:41 -04:00
43c6fe672f v12.44.21 2021-06-23 16:54:41 +03:00
486cae1aaa Merge pull request #2282 from balena-io/cli-author
Update author details in package.json, Windows Programs and Features
2021-06-23 13:52:55 +00:00
4d588e51a7 Update author details in package.json, Windows Programs and Features
Change-type: patch
2021-06-22 00:05:25 +01:00
0035545ce1 v12.44.20 2021-06-15 00:10:05 +03:00
d559b9a5a1 Merge pull request #2265 from balena-io/balena-sdk-15.36.0
devices supported: Use new DeviceType data model as source of truth
2021-06-14 21:08:02 +00:00
e2ffc5f068 v12.44.19 2021-05-31 11:47:23 +03:00
75b2fa0e9e Merge pull request #2273 from balena-io/fast-boot-stop
Fix fast-boot module caching with read-only installation folders
2021-05-31 08:45:29 +00:00
c619ecd41b v12.44.18 2021-05-28 16:17:00 +03:00
7ed01a925b Merge pull request #2276 from balena-io/python3-install-requirements
Update advanced installation instructions
2021-05-28 13:14:39 +00:00
460022a7cf Update advanced installation instructions
Replace 'python' with 'python3' in apt-get install command line, and
refactor for clarity.

Change-type: patch
2021-05-27 20:34:48 +01:00
d15b54cf40 Fix fast-boot module caching with read-only installation folders
* Add missing fast-boot `stop()` call on CLI exit to avoid 1s timeout.
* Move `.fast-boot.json` to `~/.balena/cli-module-cache.json` to
address scenarios where the CLI is installed to a read-only folder:
- pkg's internal 'snapshot' filesystem (standalone zip package)
- Root-owned folder without write permission to regular users,
  like `/usr[/local]/lib/balena-cli` (the case of caxa-based
  installers or the GUI installer for macOS).

Change-type: patch
2021-05-27 00:23:36 +00:00
c938df2445 v12.44.17 2021-05-24 18:17:54 +03:00
bf1df05606 Merge pull request #2272 from balena-io/rgz/deploy-doc
doc: Document the image nameing scheme used by deploy
2021-05-24 15:15:46 +00:00
e04242db64 doc: Document the image nameing scheme used by deploy
When using deploy to create a release for a multi-container project one
needs to follow this to avoid triggering a build. Relevant for CI/CD scenarios
with their own build pipeline.

Change-type: patch
Signed-off-by: Robert Günzler <robertg@balena.io>
2021-05-24 16:24:26 +02:00
9265588745 v12.44.16 2021-05-18 18:35:46 +03:00
cd8070b1a6 Merge pull request #2269 from balena-io/2231-balena-preload
preload: Update balena-preload to 10.4.7
2021-05-18 15:33:56 +00:00
b17dad8c60 preload: Update balena-preload to 10.4.7
Change-type: patch
Changelog-entry: preload: Avoid hardcoded registry2 URLs with openBalena
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-05-18 11:04:58 -04:00
a254e46118 devices supported: Use new DeviceType data model as source of truth
Change-type: patch
2021-05-16 22:48:22 +00:00
6e7a0defb7 Update balena-sdk from 15.31.0 to 15.36.0
Update balena-sdk from 15.31.0 to 15.36.0

Change-type: patch
2021-05-16 19:34:40 +00:00
492dbae7f1 v12.44.15 2021-05-16 22:17:02 +03:00
ccc2c20b6d Merge pull request #2267 from Kajatin/automatic-boot-partition-number
os configure, local configure: Fix "Unsupported filesystem" error
2021-05-16 19:13:29 +00:00
501882fd26 os configure, local configure: Fix "Unsupported filesystem" error
When configuring a BalenaOS image with system connections using the CLI,
the function assumed that the boot partition was always 1. This is not
the case for every supported board. Therefore, a new function is added,
which automatically determines the boot partition number and allows
users to configure the image with system connection settings.

This change affects both the `balena local configure` and `balena os configure` commands.

Change-type: patch
2021-05-16 18:08:26 +02:00
112a7b8194 Fix forums support link in README.md
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2021-04-21 06:57:28 +00:00
203 changed files with 22988 additions and 12884 deletions

2
.gitattributes vendored
View File

@ -6,7 +6,7 @@
*.sh text eol=lf
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
doc/cli.markdown text eol=lf
docs/balena-cli.md text eol=lf
# crlf for the eol conversion test files
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf

1
.github/CODEOWNERS vendored
View File

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

View File

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

View File

@ -1,8 +1,10 @@
module.exports = {
spec: 'tests/commands/app/create.spec.ts',
reporter: 'spec',
require: 'ts-node/register/transpile-only',
file: './tests/config-tests',
timeout: 12000,
// To test only, say, 'push.spec.ts', do it as follows so that
// requests are authenticated:
// spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],
spec: 'tests/**/*.spec.ts',
};

View File

@ -5,13 +5,11 @@ npm:
os: ubuntu
architecture: x86_64
node_versions:
- "10"
- "12"
- "14"
- name: linux
os: alpine
architecture: x86_64
node_versions:
- "10"
- "12"
- "14"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -105,12 +105,12 @@ npm run update balena-sdk ^13.0.0 major
## Editing documentation files (README, INSTALL, Reference website...)
The `doc/cli.markdown` file is automatically generated by running `npm run build:doc` (which also
The `docs/balena-cli.md` file is automatically generated by running `npm run build:doc` (which also
runs as part of `npm run build`). That file is then pulled by scripts in the
[balena-io/docs](https://github.com/balena-io/docs/) GitHub repo for publishing at the [CLI
Documentation page](https://www.balena.io/docs/reference/cli/).
The content sources for the auto generation of `doc/cli.markdown` are:
The content sources for the auto generation of `docs/balena-cli.md` are:
* [Selected
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
@ -120,7 +120,7 @@ The content sources for the auto generation of `doc/cli.markdown` are:
* `lib/commands/env/add.ts`
The README file is manually edited, but subsections are automatically extracted for inclusion in
`doc/cli.markdown` by the `getCapitanoDoc()` function in
`docs/balena-cli.md` by the `getCapitanoDoc()` function in
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
@ -168,6 +168,24 @@ Optionally, these steps may be automated by installing the
npx npm-merge-driver install -g
```
## `fast-boot` and `npm link` - modifying the `node_modules` folder
During development or debugging, it is sometimes useful to temporarily modify the `node_modules`
folder (with or without making the respective changes to the `npm-shrinkwrap.json` file),
replacing dependencies with different versions. This can be achieved with the `npm link`
command, or by manually editing or copying files to the `node_modules` folder.
Unexpected behavior may then be observed because of the CLI's use of the
[fast-boot2](https://www.npmjs.com/package/fast-boot2) package that caches module resolution.
`fast-boot2` is configured in `lib/fast-boot.ts` to automatically invalidate the cache if
changes are made to the `package.json` or `npm-shrinkwrap.json` files, but the cache won't
be automatically invalidated if `npm link` is used or if manual modifications are made to the
`node_modules` folder. In this situation:
* Manually delete the module cache file (typically `~/.balena/cli-module-cache.json`), or
* Use the `bin/balena-dev` entry point (instead of `bin/balena`) as it does not activate
`fast-boot2`.
## TypeScript and oclif
The CLI currently contains a mix of plain JavaScript and
@ -241,3 +259,11 @@ gotchas to bear in mind:
`node_modules/balena-sdk/node_modules/balena-errors`
In the case of subclasses of `TypedError`, a string comparison may be used instead:
`error.name === 'BalenaApplicationNotFound'`
## Further debugging notes
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
replace: `spec: 'tests/**/*.spec.ts',`
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`

View File

@ -76,24 +76,48 @@ as described above.
If you are a Node.js developer, you may wish to install the balena CLI via [npm](https://www.npmjs.com).
The npm installation involves building native (platform-specific) binary modules, which require
some additional development tools to be installed first:
some development tools to be installed first, as follows.
* [Node.js](https://nodejs.org/) version 10 (min **10.20.0**) or 12 (version 14 is not yet fully supported)
* **Linux, macOS** and **Windows Subsystem for Linux (WSL):**
Installing Node via [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) is recommended.
When the "system" or "default" Node.js and npm packages are installed with "apt-get" in Linux
distributions like Ubuntu, users often report permission or compilation errors when running
"npm install".
* [Python 2.7](https://www.python.org/), [git](https://git-scm.com/), [make](https://www.gnu.org/software/make/), [g++](https://gcc.gnu.org/)
* **Linux** and **Windows Subsystem for Linux (WSL):**
`sudo apt-get install -y python git make g++`
* **macOS:** install Apple's Command Line Tools by running on a Terminal window:
`xcode-select --install`
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
> **Versions 13 and later are not yet fully supported.**
On **Windows (not WSL),** the dependencies above and additional ones can be met by installing:
### Install development tools
* Node.js from the [Nodejs.org download page](https://nodejs.org/en/download/).
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++`, `7z` and more:
#### **Linux or WSL** (Windows Subsystem for Linux)
```sh
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 12
```
The `curl` command line above uses
[nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script) to install
Node.js, instead of using `apt-get`. Installing Node.js through `apt-get` is a common source of
problems from permission errors to conflict with other system packages, and therefore not
recommended.
#### **macOS**
* Download and install Apple's Command Line Tools from https://developer.apple.com/downloads/
* Install Node.js through [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script):
```sh
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 12
```
#### **Windows** (not WSL)
Install:
* Node.js v12 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
* If you'd like the ability to switch between Node.js versions, install
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
instead.
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
* `pacman -S git gcc make openssh p7zip`
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based
@ -101,26 +125,23 @@ On **Windows (not WSL),** the dependencies above and additional ones can be met
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
* The Windows Driver Kit (WDK), which is needed to compile some native Node modules. It is **not**
necessary to install Visual Studio, only the WDK, which is "step 2" in the following guides:
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-wdk-for-windows-10-version-1903)
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-refreshed-wdk-for-windows-10-version-2004)
* [WDK for earlier versions of Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads#step-2-install-the-wdk)
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package (which
provides Python 2.7 and more), by running the following command on an [administrator
console](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/):
`npm install -g --production windows-build-tools`
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package,
by running the following command on an [administrator
console](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/):
`npm install --global --production windows-build-tools`
With these dependencies in place, the balena CLI installation command is:
### Install the balena CLI
After installing the development tools, install the balena CLI with:
```sh
$ npm install balena-cli -g --production --unsafe-perm
$ npm install balena-cli --global --production --unsafe-perm
```
`--unsafe-perm` is required when `npm install` is executed as the root user, or on systems where
the global install directory is not user-writable. It allows npm install steps to download and save
prebuilt native binaries, and also allows the execution of npm scripts like `postinstall` that are
used to patch dependencies. It is usually possible to omit `--unsafe-perm` if installing under a
regular (non-root) user account, especially if using a user-managed node installation such as
[nvm](https://github.com/creationix/nvm).
`--unsafe-perm` is needed when `npm install` is executed as the `root` user (e.g. in a Docker
container) in order to allow npm scripts like `postinstall` to be executed.
## Additional Dependencies
@ -134,7 +155,7 @@ system:
Where Docker or balenaEngine are required, they may be installed on the local machine (where the
balena CLI is executed), on a remote server, or on a balenaOS device running a [balenaOS development
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images)). Reasons why this
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images). Reasons why this
may be desirable include:
* To avoid having to install Docker on the development machine / laptop.

View File

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

View File

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

View File

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

View File

@ -57,7 +57,7 @@ guide](https://docs.docker.com/compose/completion/) for system setup instruction
## Logging in
Several CLI commands require access to your balenaCloud account, for example in order to push a
new release to your application. Those commands require creating a CLI login session by running:
new release to your fleet. Those commands require creating a CLI login session by running:
```sh
$ balena login
@ -144,7 +144,7 @@ To learn more, troubleshoot issues, or to contact us for support:
* Check the [masterclass tutorials](https://www.balena.io/docs/learn/more/masterclasses/overview/)
* Check our [FAQ / troubleshooting document](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md)
* Ask us a question through the [balenaCloud forum](https://forums.balena.io/c/balena-cloud)
* Ask us a question in the [balena forums](https://forums.balena.io/c/product-support)
For CLI bug reports or feature requests, check the
[CLI GitHub issues](https://github.com/balena-io/balena-cli/issues/).
@ -156,14 +156,18 @@ of major, minor and patch version releases.
The latest release of a major version of the balena CLI will remain compatible with
the balenaCloud backend services for at least one year from the date when the
following major version is released. For example, balena CLI v10.17.5, as the
latest v10 release, would remain compatible with the balenaCloud backend for one
year from the date when v11.0.0 is released.
following major version is released. For example, balena CLI v11.36.0, as the
latest v11 release, would remain compatible with the balenaCloud backend for one
year from the date when v12.0.0 was released.
At the end of this period, the older major version is considered deprecated and
some of the functionality that depends on balenaCloud services may stop working
at any time.
Users are encouraged to regularly update the balena CLI to the latest version.
Half way through to that period (6 months after the release of the next major
version), older major versions of the balena CLI will start printing a deprecation
warning message when it is used interactively (when `stderr` is attached to a TTY
device file). At the end of that period, older major versions will exit with an
error message unless the `--unsupported` flag is used. This behavior was
introduced in CLI version 12.47.0 and is also documented by `balena help`.
To take advantage of the latest backend features and ensure compatibility, users
are encouraged to regularly update the balena CLI to the latest version.
## Contributing (including editing documentation files)

View File

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

View File

@ -17,30 +17,31 @@
import type { JsonVersions } from '../lib/commands/version';
import { run as oclifRun } from '@oclif/dev-cli';
import { run as oclifRun } from 'oclif';
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;
@ -63,7 +64,7 @@ const standaloneZips: PathByPlatform = {
const oclifInstallers: PathByPlatform = {
darwin: dPath('macos', `balena-${version}.pkg`),
win32: dPath('win', `balena-${version}-${arch}.exe`),
win32: dPath('win32', `balena-${version}-${arch}.exe`),
};
const renamedOclifInstallers: PathByPlatform = {
@ -95,6 +96,7 @@ async function diffPkgOutput(pkgOut: string) {
'> pkg@',
'> Fetching base Node.js binaries',
' fetched-',
'prebuild-install WARN install No prebuilt binaries found',
];
const modulesRE =
process.platform === 'win32'
@ -246,7 +248,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 +275,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)');
}
@ -295,7 +311,7 @@ async function zipPkg() {
archive.on('warning', console.warn);
archive.pipe(outputStream);
archive.finalize();
archive.finalize().catch(reject);
});
}
@ -307,6 +323,10 @@ async function signFilesForNotarization() {
if (!item.stats.isFile()) {
return;
}
if (path.basename(item.path).endsWith('.node.bak')) {
console.log('Removing pkg .node.bak file', item.path);
fs.unlinkSync(item.path);
}
if (
path.basename(item.path).endsWith('.zip') &&
path.dirname(item.path).includes('test')
@ -411,8 +431,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',
@ -443,7 +461,7 @@ async function notarizeMacInstaller(): Promise<void> {
}
/**
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
* Run the `oclif pack:win` or `pack:macos` command (depending on the value
* of process.platform) to generate the native installers (which end up under
* the 'dist' folder). There are some harcoded options such as selecting only
* 64-bit binaries under Windows.
@ -473,7 +491,7 @@ export async function buildOclifInstaller() {
await signFilesForNotarization();
}
console.log('=======================================================');
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
console.log(`oclif "${packCmd}" "${packOpts.join('" "')}"`);
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
console.log('=======================================================');
await oclifRun([packCmd].concat(...packOpts));

View File

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

View File

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

View File

@ -24,15 +24,15 @@ const simplegit = require('simple-git/promise');
const ROOT = path.normalize(path.join(__dirname, '..'));
/**
* Compare the timestamp of cli.markdown with the timestamp of staged files,
* issuing an error if cli.markdown is older.
* If cli.markdown does not require updating and the developer cannot run
* Compare the timestamp of balena-cli.md with the timestamp of staged files,
* issuing an error if balena-cli.md is older.
* If balena-cli.md does not require updating and the developer cannot run
* `npm run build` on their laptop, the error message suggests a workaround
* using `touch`.
*/
async function checkBuildTimestamps() {
const git = simplegit(ROOT);
const docFile = path.join(ROOT, 'doc', 'cli.markdown');
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
const [docStat, gitStatus] = await Promise.all([
fs.stat(docFile),
git.status(),

View File

@ -6,6 +6,8 @@
*
* We don't `require('semver')` to allow this script to be run as a npm
* 'preinstall' hook, at which point no dependencies have been installed.
*
* @param {string} version
*/
function parseSemver(version) {
const match = /v?(\d+)\.(\d+).(\d+)/.exec(version);
@ -16,6 +18,10 @@ function parseSemver(version) {
return [parseInt(major, 10), parseInt(minor, 10), parseInt(patch, 10)];
}
/**
* @param {string} v1
* @param {string} v2
*/
function semverGte(v1, v2) {
let v1Array = parseSemver(v1);
let v2Array = parseSemver(v2);
@ -41,17 +47,25 @@ function checkNpmVersion() {
// the reason is that it would unnecessarily prevent end users from
// using npm v6.4.1 that ships with Node 8. (It is OK for the
// shrinkwrap file to get damaged if it is not going to be reused.)
console.error(`\
-------------------------------------------------------------------------------
throw new Error(`\
-----------------------------------------------------------------------------
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
At this point, however, your 'npm-shrinkwrap.json' file has already been
damaged. Please revert it to the master branch state with a command such as:
"git checkout master -- npm-shrinkwrap.json"
Then re-run "npm install" using npm version ${requiredVersion} or later.
-------------------------------------------------------------------------------`);
process.exit(1);
-----------------------------------------------------------------------------`);
}
}
checkNpmVersion();
function main() {
try {
checkNpmVersion();
} catch (e) {
console.error(e.message || e);
process.exitCode = 1;
}
}
main();

View File

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

View File

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

View File

@ -11,8 +11,7 @@ const validateChangeType = (maybeChangeType: string = 'minor') => {
case 'major':
return maybeChangeType;
default:
console.error(`Invalid change type: '${maybeChangeType}'`);
return process.exit(1);
throw new Error(`Invalid change type: '${maybeChangeType}'`);
}
};
@ -37,8 +36,8 @@ const run = async (cmd: string) => {
}
resolve({ stdout, stderr });
});
p.stdout.pipe(process.stdout);
p.stderr.pipe(process.stderr);
p.stdout?.pipe(process.stdout);
p.stderr?.pipe(process.stderr);
});
};
@ -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,14 @@ async function main() {
);
}
async function main() {
try {
await $main();
} catch (e) {
console.error(e);
process.exitCode = 1;
}
}
// tslint:disable-next-line:no-floating-promises
main();

View File

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

View File

@ -9,14 +9,15 @@ process.env.UV_THREADPOOL_SIZE = '64';
// Disable oclif registering ts-node
process.env.OCLIF_TS_NODE = 0;
// Use fast-boot to cache require lookups, speeding up startup
require('fast-boot2').start({
cacheScope: __dirname + '/..',
cacheFile: __dirname + '/.fast-boot.json',
});
async function run() {
// Use fast-boot to cache require lookups, speeding up startup
await require('../build/fast-boot').start();
// Set the desired es version for downstream modules that support it
require('@balena/es-version').set('es2018');
// Set the desired es version for downstream modules that support it
require('@balena/es-version').set('es2018');
// Run the CLI
require('../build/app').run();
// Run the CLI
await require('../build/app').run();
}
run();

View File

@ -11,6 +11,22 @@
// operations otherwise, if the pool runs out.
process.env.UV_THREADPOOL_SIZE = '64';
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
// * fast-boot2's cacheKiller option is configured to include the timestamps of
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
// behavior when changes are made to dependencies during development. This is
// generally a good thing, however, `balena-dev` (a few lines below) edits
// `package.json` to modify oclif paths, and this results in cache
// invalidation and a performance hit rather than speedup.
// * Even if the timestamps are removed from cacheKiller, so that there is no
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
// * `fast-boot` causes unexpected behavior when used with `npm link` or
// when the `node_modules` folder is manually modified (affecting transitive
// dependencies) during development (e.g. bug investigations). A workaround
// is to use `balena-dev` without `fast-boot`. See also notes in
// `CONTRIBUTING.md`.
const path = require('path');
const rootDir = path.join(__dirname, '..');
@ -31,12 +47,6 @@ process.on('SIGINT', function () {
process.exit();
});
// Use fast-boot to cache require lookups, speeding up startup
require('fast-boot2').start({
cacheScope: __dirname + '/..',
cacheFile: '.fast-boot.json',
});
// Set the desired es version for downstream modules that support it
require('@balena/es-version').set('es2018');

View File

@ -8,18 +8,19 @@ _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=( 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 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 )
config_cmds=( generate inject read reconfigure write )
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 )
@ -42,9 +43,6 @@ _balena_sec_cmds() {
"api-key")
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
;;
"app")
_describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0
;;
"config")
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
;;
@ -57,6 +55,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 +70,9 @@ _balena_sec_cmds() {
"os")
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
;;
"release")
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
;;
"tag")
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
;;

View File

@ -7,18 +7,19 @@ _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="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 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"
config_cmds="generate inject read reconfigure write"
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"
@ -36,9 +37,6 @@ _balena_complete()
api-key)
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
;;
app)
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
;;
config)
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
;;
@ -51,6 +49,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 +64,9 @@ _balena_complete()
os)
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
;;
release)
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
;;
tag)
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
;;

View File

@ -1,112 +0,0 @@
# Provisioning balena devices in automated (non-interactive) mode
This document describes how to run the `device init` command in non-interactive mode.
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
```
You can run this command as many times as you need, putting the new medium (SD card / USB stick) each time.
But before you can run it you need to collect the parameters and build the configuration file. Keep reading to figure out how to do it.
## Collect all the required parameters.
1. `DEVICE_TYPE`. Run
```bash
balena devices supported
```
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. `OS_VERSION`. Run
```bash
balena os versions DEVICE_TYPE
```
and pick the version that you need, like _v2.0.6+rev1.prod_.
_Note_ that even though we support _semver ranges_ it's recommended to use the exact version when doing the automated provisioning as it
guarantees full compatibility between the steps.
1. `DRIVE`. Plug in your target medium (SD card or the USB stick, depending on your device type) and run
```bash
balena util available-drives
```
and get the drive name, like _/dev/sdb_ or _/dev/mmcblk0_.
The balena CLI will not display the system drives to protect you,
but still please check very carefully that you've picked the correct drive as it will be erased during the provisioning process.
Now we have all the parameters -- time to build the config file.
## Build the config file
Interactive device provisioning process often includes collecting some extra device configuration, like the networking mode and wifi credentials.
To skip this interactive step we need to buid this configuration once and save it to the JSON file for later reuse.
Let's say we will place it into the `CONFIG_FILE` path, like _./balena-os/raspberrypi3-config.json_.
We also need to put the OS image somewhere, let's call this path `OS_IMAGE_PATH`, it can be something like _./balena-os/raspberrypi3-v2.0.6+rev1.prod.img_.
1. First we need to download the OS image once. That's needed for building the config, and will speedup the subsequent operations as the downloaded OS image is placed into the local cache.
Run:
```bash
balena os download DEVICE_TYPE --output OS_IMAGE_PATH --version OS_VERSION
```
1. Now we're ready to build the config:
```bash
balena os build-config OS_IMAGE_PATH DEVICE_TYPE --output CONFIG_FILE
```
This will run you through the interactive configuration wizard and in the end save the generated config as `CONFIG_FILE`. You can then verify it's not empty:
```bash
cat CONFIG_FILE
```
## Done
Now you're ready to run the command in the beginning of this guide.
Please note again that all of these steps only need to be done once (unless you need to change something), and once all the parameters are collected the main init command can be run unchanged.
But there are still some nuances to cover, please read below.
## Nuances
### `sudo` password on *nix systems
In order to write the image to the raw device we need the root permissions, this is unavoidable.
To improve the security we only run the minimal subcommand with `sudo`.
This means that with the default setup you're interrupted closer to the end of the device init process to enter your sudo password for this subcommand to work.
There are several ways to eliminate it and make the process fully non-interactive.
#### Option 1: make passwordless sudo.
Obviously you shouldn't do that if the machine you're working on has access to any sensitive resources or information.
But if you're using a machine dedicated to balena provisioning this can be fine, and also the simplest thing to do.
#### Option 2: `NOPASSWD` directive
You can configure the `balena` CLI command to be sudo-runnable without the password. Check [this post](https://askubuntu.com/questions/159007/how-do-i-run-specific-sudo-commands-without-a-password) for an example.
### Extra initialization config
As of June 2017 all the supported devices should not require any other interactive configuration.
But by the design of our system it is _possible_ (though it doesn't look very likely it's going to happen any time soon) that some extra initialization options may be requested for the specific device types.
If that is the case please raise the issue in the balena CLI repository and the maintainers will add the necessary options to build the similar JSON config for this step.

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
const gulp = require('gulp');
const inlinesource = require('gulp-inline-source');
const OPTIONS = {
files: {
pages: 'lib/auth/pages/*.ejs',
},
};
gulp.task('pages', () =>
gulp
.src(OPTIONS.files.pages)
.pipe(inlinesource())
.pipe(gulp.dest('build/auth/pages')),
);

View File

@ -16,8 +16,14 @@
*/
import * as packageJSON from '../package.json';
import {
AppOptions,
checkDeletedCommand,
preparseArgs,
unsupportedFlag,
} from './preparser';
import { CliSettings } from './utils/bootstrap';
import { onceAsync, stripIndent } from './utils/lazy';
import { onceAsync } from './utils/lazy';
/**
* Sentry.io setup
@ -27,6 +33,7 @@ export const setupSentry = onceAsync(async () => {
const config = await import('./config');
const Sentry = await import('@sentry/node');
Sentry.init({
autoSessionTracking: false,
dsn: config.sentryDsn,
release: packageJSON.version,
});
@ -43,13 +50,8 @@ export const setupSentry = onceAsync(async () => {
async function checkNodeVersion() {
const validNodeVersions = packageJSON.engines.node;
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
console.warn(stripIndent`
------------------------------------------------------------------------------
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
This may cause unexpected behavior. To upgrade Node, visit:
https://nodejs.org/en/download/
------------------------------------------------------------------------------
`);
const { getNodeEngineVersionWarn } = await import('./utils/messages');
console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
}
}
@ -75,11 +77,13 @@ export function setMaxListeners(maxListeners: number) {
/** Selected CLI initialization steps */
async function init() {
if (process.env.BALENARC_NO_SENTRY) {
console.error(`WARN: disabling Sentry.io error reporting`);
if (process.env.DEBUG) {
console.error(`WARN: disabling Sentry.io error reporting`);
}
} else {
await setupSentry();
}
checkNodeVersion();
await checkNodeVersion();
const settings = new CliSettings();
@ -89,43 +93,70 @@ async function init() {
setupBalenaSdkSharedOptions(settings);
// check for CLI updates once a day
(await import('./utils/update')).notify();
if (!process.env.BALENARC_OFFLINE_MODE) {
(await import('./utils/update')).notify();
}
}
/** Execute the oclif parser and the CLI command. */
async function oclifRun(
command: string[],
options: import('./preparser').AppOptions,
) {
const { CustomMain } = await import('./utils/oclif-utils');
const runPromise = CustomMain.run(command).then(
() => {
if (!options.noFlush) {
return require('@oclif/command/flush');
}
},
(error) => {
// oclif sometimes exits with ExitError code 0 (not an error)
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;
try {
await CustomMain.run(command);
} catch (error) {
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
// for example the `balena help` command.
// (Avoid `error instanceof ExitError` here for the reasons explained
// in the CONTRIBUTING.md file regarding the `instanceof` operator.)
if (error.oclif?.exit === 0) {
return;
isEEXIT = true;
} else {
throw error;
}
},
);
}
if (shouldFlush) {
await import('@oclif/command/flush');
}
// TODO: figure out why we need to call fast-boot stop() here, in
// addition to calling it in the main `run()` function in this file.
// If it is not called here as well, there is a process exit delay of
// 1 second when the fast-boot2 cache is modified (1 second is the
// default cache saving timeout). Try for example `balena help`.
// I have found that, when oclif's `Error: EEXIT: 0` is caught in
// the try/catch block above, execution does not get past the
// Promise.all() call below, but I don't understand why.
if (isEEXIT) {
(await import('./fast-boot')).stop();
}
})(!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');
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
'./utils/bootstrap'
);
setOfflineModeEnvVars();
normalizeEnvVars();
// The 'pkgExec' special/internal command provides a Node.js interpreter
@ -136,8 +167,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));
@ -146,6 +175,13 @@ export async function run(
} catch (err) {
await (await import('./errors')).handleError(err);
} finally {
try {
(await import('./fast-boot')).stop();
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] Stopping fast-boot: ${e}`);
}
}
// Windows fix: reading from stdin prevents the process from exiting
process.stdin.pause();
}

View File

@ -56,7 +56,7 @@ export async function login({ host = '127.0.0.1', port = 0 }) {
console.info(`Opening web browser for URL:\n${loginUrl}`);
const open = await import('open');
open(loginUrl, { wait: false });
await open(loginUrl, { wait: false });
const balena = getBalenaSdk();
const token = await loginServer.awaitForToken();

View File

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

View File

@ -14,57 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as _ from 'lodash';
import * as url from 'url';
import { getBalenaSdk } from '../utils/lazy';
/**
* @summary Get dashboard CLI login URL
* @function
* @protected
* Get dashboard CLI login URL
*
* @param {String} callbackUrl - callback url
* @fulfil {String} - dashboard login url
* @returns {Promise}
*
* @example
* utils.getDashboardLoginURL('http://127.0.0.1:3000').then (url) ->
* console.log(url)
* @param callbackUrl - Callback url, e.g. 'http://127.0.0.1:3000'
* @returns Dashboard login URL, e.g.:
* 'https://dashboard.balena-cloud.com/login/cli/http%253A%252F%252F127.0.0.1%253A59581%252Fauth'
*/
export const getDashboardLoginURL = (callbackUrl: string) => {
export async function getDashboardLoginURL(
callbackUrl: string,
): Promise<string> {
// Encode percentages signs from the escaped url
// characters to avoid angular getting confused.
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25');
return getBalenaSdk()
.settings.get('dashboardUrl')
.then((dashboardUrl) =>
url.resolve(dashboardUrl, `/login/cli/${callbackUrl}`),
);
};
const [{ URL }, dashboardUrl] = await Promise.all([
import('url'),
getBalenaSdk().settings.get('dashboardUrl'),
]);
return new URL(`/login/cli/${callbackUrl}`, dashboardUrl).href;
}
/**
* @summary Log in using a token, but only if the token is valid
* @function
* @protected
* Log in using a token, but only if the token is valid.
*
* @description
* This function checks that the token is not only well-structured
* but that it also authenticates with the server successfully.
*
* If authenticated, the token is persisted, if not then the previous
* login state is restored.
*
* @param {String} token - session token or api key
* @fulfil {Boolean} - whether the login was successful or not
* @returns {Promise}
*
* utils.loginIfTokenValid('...').then (loggedIn) ->
* if loggedIn
* console.log('Token is valid!')
* @param token - session token or api key
* @returns whether the login was successful or not
*/
export const loginIfTokenValid = async (token: string): Promise<boolean> => {
if (_.isEmpty(token?.trim())) {
export async function loginIfTokenValid(token?: string): Promise<boolean> {
token = (token || '').trim();
if (!token) {
return false;
}
const balena = getBalenaSdk();
@ -86,4 +73,4 @@ export const loginIfTokenValid = async (token: string): Promise<boolean> => {
}
}
return isLoggedIn;
};
}

View File

@ -16,7 +16,12 @@
*/
import Command from '@oclif/command';
import { InsufficientPrivilegesError } from './errors';
import {
InsufficientPrivilegesError,
NotAvailableInOfflineModeError,
} from './errors';
import { stripIndent } from './utils/lazy';
import * as output from './framework/output';
export default abstract class BalenaCommand extends Command {
/**
@ -40,6 +45,13 @@ export default abstract class BalenaCommand extends Command {
*/
public static authenticated = false;
/**
* Require an internet connection to run.
* When set to true, command will exit with an error
* if user is running in offline mode (BALENARC_OFFLINE_MODE).
*/
public static offlineCompatible = false;
/**
* Accept piped input.
* When set to true, command will read from stdin during init
@ -97,6 +109,29 @@ export default abstract class BalenaCommand extends Command {
}
}
/**
* Throw NotAvailableInOfflineModeError if in offline mode.
*
* Called automatically if `onlineOnly=true`.
* Can be called explicitly by command implementation, if e.g.:
* - check should only be done conditionally
* - other code needs to execute before check
*
* Note, currently public to allow use outside of derived commands
* (as some command implementations require this. Can be made protected
* if this changes).
*
* @throws {NotAvailableInOfflineModeError}
*/
public static checkNotUsingOfflineMode() {
if (process.env.BALENARC_OFFLINE_MODE) {
throw new NotAvailableInOfflineModeError(stripIndent`
This command requires an internet connection, and cannot be used in offline mode.
To leave offline mode, unset the BALENARC_OFFLINE_MODE environment variable.
`);
}
}
/**
* Read stdin contents and make available to command.
*
@ -125,8 +160,16 @@ export default abstract class BalenaCommand extends Command {
await BalenaCommand.checkLoggedIn();
}
if (!ctr.offlineCompatible) {
BalenaCommand.checkNotUsingOfflineMode();
}
if (ctr.readStdin) {
await this.getStdin();
}
}
protected outputMessage = output.outputMessage;
protected outputData = output.outputData;
protected printTitle = output.printTitle;
}

View File

@ -1,96 +0,0 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
interface ExtendedApplication extends ApplicationWithDeviceType {
device_count?: number;
online_devices?: number;
}
interface FlagsDef {
help: void;
verbose: boolean;
}
export default class AppsCmd extends Command {
public static description = stripIndent`
List all applications.
list all your balena applications.
For detailed information on a particular application,
use \`balena app <application>\` instead.
`;
public static examples = ['$ balena apps'];
public static usage = 'apps';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
verbose: flags.boolean({
default: false,
char: 'v',
description: 'No-op since release v12.0.0',
}),
};
public static authenticated = true;
public static primary = true;
public async run() {
this.parse<FlagsDef, {}>(AppsCmd);
const balena = getBalenaSdk();
// Get applications
const applications = (await balena.models.application.getAll({
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
})) as ExtendedApplication[];
const _ = await import('lodash');
// Add extended properties
applications.forEach((application) => {
application.device_count = application.owns__device?.length ?? 0;
application.online_devices = _.sumBy(application.owns__device, (d) =>
d.is_online === true ? 1 : 0,
);
// @ts-expect-error
application.device_type = application.is_for__device_type[0].slug;
});
// Display
console.log(
getVisuals().table.horizontal(applications, [
'id',
'app_name',
'slug',
'device_type',
'online_devices',
'device_count',
]),
);
}
}

View File

@ -18,6 +18,7 @@
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 {
@ -33,7 +34,7 @@ import { dockerCliFlags } from '../utils/docker';
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 +52,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 +66,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 +92,7 @@ ${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',
}),
fleet: cf.fleet,
...composeCliFlags,
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
@ -109,10 +107,8 @@ ${dockerignoreHelp}
BuildCmd,
);
await Command.checkLoggedInIf(!!options.application);
await Command.checkLoggedInIf(!!options.fleet);
// 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();
@ -155,14 +151,12 @@ ${dockerignoreHelp}
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
// Validate option combinations
if (
(opts.application == null &&
(opts.arch == null || opts.deviceType == null)) ||
(opts.application != null &&
(opts.arch != null || opts.deviceType != null))
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
(opts.fleet != null && (opts.arch != null || opts.deviceType != null))
) {
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)',
);
}
@ -183,9 +177,9 @@ ${dockerignoreHelp}
}
protected async getAppAndResolveArch(opts: FlagsDef) {
if (opts.application) {
if (opts.fleet) {
const { getAppWithArch } = await import('../utils/helpers');
const app = await getAppWithArch(opts.application);
const app = await getAppWithArch(opts.fleet);
opts.arch = app.arch;
opts.deviceType = app.is_for__device_type[0].slug;
return app;
@ -233,7 +227,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 +241,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 +259,6 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore,
multiDockerignore: composeOpts.multiDockerignore,
});
}

View File

@ -19,13 +19,13 @@ 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, devModeInfo } from '../../utils/messages';
import type { PineDeferred } from 'balena-sdk';
interface FlagsDef {
version: string; // OS version
application?: string;
app?: string; // application alias
fleet?: string;
dev?: boolean; // balenaOS development variant
device?: string;
deviceApiKey?: string;
deviceType?: string;
@ -36,6 +36,7 @@ interface FlagsDef {
wifiSsid?: string;
wifiKey?: string;
appUpdatePollInterval?: string;
'provisioning-key-name'?: string;
help: void;
}
@ -43,16 +44,17 @@ 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.
${devModeInfo.split('\n').join('\n\t\t')}
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 configure an image for a fleet of mixed device types, use the --fleet option
alongside the --deviceType option 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')}
`;
@ -60,13 +62,12 @@ export default class ConfigGenerateCmd extends Command {
public static examples = [
'$ balena config generate --device 7cf02a6 --version 2.12.7',
'$ 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 --deviceApiKey <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 myorg/fleet --version 2.12.7 --dev',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
];
public static usage = 'config generate';
@ -76,20 +77,20 @@ 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'],
}),
fleet: { ...cf.fleet, exclusive: ['device'] },
dev: cf.dev,
device: {
...cf.device,
exclusive: ['fleet', 'provisioning-key-name'],
},
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 +114,11 @@ 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 device variables)',
}),
'provisioning-key-name': flags.string({
description: 'custom key name assigned to generated provisioning api key',
exclusive: ['device'],
}),
help: cf.help,
};
@ -143,8 +148,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;
@ -152,7 +157,7 @@ export default class ConfigGenerateCmd extends Command {
resourceDeviceType = device.is_of__device_type[0].slug;
} else {
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
application = (await getApplication(balena, options.application!, {
application = (await getApplication(balena, options.fleet!, {
$expand: {
is_for__device_type: { $select: 'slug' },
},
@ -167,7 +172,7 @@ export default class ConfigGenerateCmd extends Command {
);
// Check compatibility if application and deviceType provided
if (options.application && options.deviceType) {
if (options.fleet && options.deviceType) {
const appDeviceManifest = await balena.models.device.getManifestBySlug(
resourceDeviceType,
);
@ -177,7 +182,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.fleet}`,
);
}
}
@ -186,9 +191,11 @@ export default class ConfigGenerateCmd extends Command {
// Pass params as an override: if there is any param with exactly the same name as a
// required option, that value is used (and the corresponding question is not asked)
const answers = await getCliForm().run(deviceManifest.options, {
override: options,
override: { ...options, app: options.fleet, application: options.fleet },
});
answers.version = options.version;
answers.developmentMode = options.dev;
answers.provisioningKeyName = options['provisioning-key-name'];
// Generate config
const { generateDeviceConfig, generateApplicationConfig } = await import(
@ -218,7 +225,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,21 +233,19 @@ 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');
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
if (options.device == null && options.application == null) {
if (options.device == null && options.fleet == null) {
throw new ExpectedError(this.missingDeviceOrAppMessage);
}
if (!options.application && options.deviceType) {
if (!options.fleet && options.deviceType) {
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
}
const { validateDevOptionAndWarn } = await import('../../utils/config');
await validateDevOptionAndWarn(options.dev, options.version);
}
}

View File

@ -21,7 +21,7 @@ import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type: string;
type?: string;
drive?: string;
help: void;
}
@ -32,15 +32,18 @@ interface ArgsDef {
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 a balenaOS image or attached media.
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 balenaOS image file or attached SD card or
USB stick.
Documentation for the balenaOS 'config.json' file can be found at:
https://www.balena.io/docs/reference/OS/configuration/
`;
public static examples = [
'$ balena config inject my/config.json --type raspberrypi3',
'$ balena config inject my/config.json --type raspberrypi3 --drive /dev/disk2',
'$ balena config inject my/config.json',
'$ balena config inject my/config.json --drive /dev/disk2',
];
public static args = [
@ -54,21 +57,20 @@ export default class ConfigInjectCmd extends Command {
public static usage = 'config inject <file>';
public static flags: flags.Input<FlagsDef> = {
type: cf.deviceType,
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};
public static authenticated = true;
public static root = true;
public static offlineCompatible = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
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'));
@ -80,7 +82,7 @@ export default class ConfigInjectCmd extends Command {
);
const config = await import('balena-config-json');
await config.write(drive, options.type, configJSON);
await config.write(drive, '', configJSON);
console.info('Done');
}

View File

@ -21,49 +21,58 @@ import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type: string;
type?: string;
drive?: string;
help: void;
json: boolean;
}
export default class ConfigReadCmd extends Command {
public static description = stripIndent`
Read the configuration of a device or OS image.
Read the config.json file of a balenaOS image or attached media.
Read the config.json file from the mounted filesystem,
e.g. the SD card of a provisioned device or balenaOS image.
Read the 'config.json' file of a balenaOS image file or attached SD card or
USB stick.
Documentation for the balenaOS 'config.json' file can be found at:
https://www.balena.io/docs/reference/OS/configuration/
`;
public static examples = [
'$ balena config read --type raspberrypi3',
'$ balena config read --type raspberrypi3 --drive /dev/disk2',
'$ balena config read',
'$ balena config read --drive /dev/disk2',
'$ balena config read --drive balena.img',
];
public static usage = 'config read';
public static flags: flags.Input<FlagsDef> = {
type: cf.deviceType,
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
json: cf.json,
};
public static authenticated = true;
public static root = true;
public static offlineCompatible = true;
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'));
await safeUmount(drive);
const config = await import('balena-config-json');
const configJSON = await config.read(drive, options.type);
const configJSON = await config.read(drive, '');
const prettyjson = await import('prettyjson');
console.info(prettyjson.render(configJSON));
if (options.json) {
console.log(JSON.stringify(configJSON, null, 4));
} else {
const prettyjson = await import('prettyjson');
console.log(prettyjson.render(configJSON));
}
}
}

View File

@ -21,54 +21,74 @@ import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type: string;
type?: string;
drive?: string;
advanced: boolean;
help: void;
version?: string;
}
export default class ConfigReconfigureCmd extends Command {
public static description = stripIndent`
Interactively reconfigure a device or OS image.
Interactively reconfigure a balenaOS image file or attached media.
Interactively reconfigure a provisioned device or OS image.
Interactively reconfigure a balenaOS image file or attached media.
This command extracts the device UUID from the 'config.json' file of the
chosen balenaOS image file or attached media, and then passes the UUID as
the '--device' argument to the 'balena os configure' command.
For finer-grained or scripted control of the operation, use the
'balena config read' and 'balena os configure' commands separately.
`;
public static examples = [
'$ balena config reconfigure --type raspberrypi3',
'$ balena config reconfigure --type raspberrypi3 --advanced',
'$ balena config reconfigure --type raspberrypi3 --drive /dev/disk2',
'$ balena config reconfigure',
'$ balena config reconfigure --drive /dev/disk3',
'$ balena config reconfigure --drive balena.img --advanced',
];
public static usage = 'config reconfigure';
public static flags: flags.Input<FlagsDef> = {
type: cf.deviceType,
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
advanced: flags.boolean({
description: 'show advanced commands',
char: 'v',
}),
help: cf.help,
version: flags.string({
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
}),
};
public static authenticated = true;
public static root = true;
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'));
await safeUmount(drive);
const config = await import('balena-config-json');
const { uuid } = await config.read(drive, options.type);
const { uuid } = await config.read(drive, '');
await safeUmount(drive);
if (!uuid) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(
`Error: UUID not found in 'config.json' file for '${drive}'`,
);
}
const configureCommand = ['os', 'configure', drive, '--device', uuid];
if (options.version) {
configureCommand.push('--version', options.version);
}
if (options.advanced) {
configureCommand.push('--advanced');
}

View File

@ -21,7 +21,7 @@ import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type: string;
type?: string;
drive?: string;
help: void;
}
@ -33,16 +33,19 @@ interface ArgsDef {
export default class ConfigWriteCmd extends Command {
public static description = stripIndent`
Write a key-value pair to configuration of a device or OS image.
Write a key-value pair to the config.json file of an OS image or attached media.
Write a key-value pair to the config.json file on the mounted filesystem,
e.g. the SD card of a provisioned device or balenaOS image.
Write a key-value pair to the 'config.json' file of a balenaOS image file or
attached SD card or USB stick.
Documentation for the balenaOS 'config.json' file can be found at:
https://www.balena.io/docs/reference/OS/configuration/
`;
public static examples = [
'$ balena config write --type raspberrypi3 username johndoe',
'$ balena config write --type raspberrypi3 --drive /dev/disk2 username johndoe',
'$ balena config write --type raspberrypi3 files.network/settings "..."',
'$ balena config write ntpServers "0.resinio.pool.ntp.org 1.resinio.pool.ntp.org"',
'$ balena config write --drive /dev/disk2 hostname custom-hostname',
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
];
public static args = [
@ -61,37 +64,46 @@ export default class ConfigWriteCmd extends Command {
public static usage = 'config write <key> <value>';
public static flags: flags.Input<FlagsDef> = {
type: cf.deviceType,
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};
public static authenticated = true;
public static root = true;
public static offlineCompatible = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
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'));
await safeUmount(drive);
const config = await import('balena-config-json');
const configJSON = await config.read(drive, options.type);
const configJSON = await config.read(drive, '');
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, '', configJSON);
});
console.info('Done');
}
/** Call Lodash's _.setWith(). Moved here for ease of testing. */
static updateConfigJson(configJSON: object, key: string, value: string) {
const _ = require('lodash') as typeof import('lodash');
// note: _.setWith() is needed instead of _.set() because, given a key
// like `os.udevRules.101`, _.set() creates a udevRules array (rather
// than a dictionary) and sets the 101st array element to value, while
// we actually want udevRules to be dictionary like { '101': value }
_.setWith(configJSON, key, value, (v) => v || {});
}
}

View File

@ -26,6 +26,7 @@ import {
registrySecretsHelp,
buildArgDeprecation,
} from '../utils/messages';
import * as ca from '../utils/common-args';
import * as compose from '../utils/compose';
import type {
BuiltImage,
@ -33,7 +34,7 @@ import type {
ComposeOpts,
Release as ComposeReleaseInfo,
} from '../utils/compose-types';
import type { DockerCliFlags } from '../utils/docker';
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import {
applyReleaseTagKeysAndValues,
buildProject,
@ -58,22 +59,23 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
build: boolean;
nologupload: boolean;
'release-tag'?: string[];
draft: boolean;
help: void;
}
interface ArgsDef {
appName: string;
fleet: string;
image?: string;
}
export default class DeployCmd extends Command {
public static description = `\
Deploy a single image or a multicontainer project to a balena application.
Deploy a single image or a multicontainer project to a balena fleet.
Usage: \`deploy <appName> ([image] | --build [--source build-dir])\`
Usage: \`deploy <fleet> ([image] | --build [--source build-dir])\`
Use this command to deploy an image or a complete multicontainer project to an
application, optionally building it first. The source images are searched for
Use this command to deploy an image or a complete multicontainer project to a
fleet, optionally building it first. The source images are searched for
(and optionally built) using the docker daemon in your development machine or
balena device. (See also the \`balena push\` command for the option of building
the image in the balenaCloud build servers.)
@ -81,13 +83,15 @@ the image in the balenaCloud build servers.)
Unless an image is specified, this command will look into the current directory
(or the one specified by --source) for a docker-compose.yml file. If one is
found, this command will deploy each service defined in the compose file,
building it first if an image for it doesn't exist. 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.
building it first if an image for it doesn't exist. Image names will be looked
up according to the scheme: \`<projectName>_<serviceName>\`.
To deploy to an app on which you're a collaborator, use
\`balena deploy <appOwnerUsername>/<appName>\`.
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 a fleet where you are a collaborator, use fleet slug including the
organization: \`balena deploy <organization>/<fleet>\`.
${registrySecretsHelp}
@ -95,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({
@ -137,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
@ -153,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) {
@ -188,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([
@ -213,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(
@ -240,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');
@ -253,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!',
);
}
@ -308,7 +319,6 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore,
multiDockerignore: composeOpts.multiDockerignore,
});
builtImagesByService = _.keyBy(builtImages, 'serviceName');
@ -328,7 +338,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);
@ -372,6 +382,8 @@ ${dockerignoreHelp}
`Bearer ${auth}`,
apiEndpoint,
!opts.shouldUploadLogs,
composeOpts.projectPath,
opts.createAsDraft,
);
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,10 +23,13 @@ import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import type { Application, Release } from 'balena-sdk';
import type { DataOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
application_name?: string;
fleet: string; // 'org/name' slug
device_type?: string;
commit?: string;
last_seen?: string;
@ -41,7 +44,7 @@ interface ExtendedDevice extends DeviceWithDeviceType {
undervoltage_detected?: boolean;
}
interface FlagsDef {
interface FlagsDef extends DataOutputOptions {
help: void;
}
@ -70,13 +73,16 @@ export default class DeviceCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
...(isV14() ? cf.dataOutputFlags : {}),
};
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 balena = getBalenaSdk();
@ -111,9 +117,10 @@ export default class DeviceCmd extends Command {
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication = device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
const belongsToApplication =
device.belongs_to__application as Application[];
device.fleet = belongsToApplication?.[0]
? belongsToApplication[0].slug
: 'N/a';
device.device_type = device.is_of__device_type[0].slug;
@ -161,37 +168,52 @@ export default class DeviceCmd extends Command {
);
}
console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
'id',
'device_type',
'status',
'is_online',
'ip_address',
'public_address',
'mac_address',
'application_name',
'last_seen',
'uuid',
'commit',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'dashboard_url',
'cpu_usage_percent',
'cpu_temp_c',
'cpu_id',
'memory_usage_mb',
'memory_total_mb',
'memory_usage_percent',
'storage_block_device',
'storage_usage_mb',
'storage_total_mb',
'storage_usage_percent',
'undervoltage_detected',
]),
);
const outputFields = [
'device_name',
'id',
'device_type',
'status',
'is_online',
'ip_address',
'public_address',
'mac_address',
'fleet',
'last_seen',
'uuid',
'commit',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'dashboard_url',
'cpu_usage_percent',
'cpu_temp_c',
'cpu_id',
'memory_usage_mb',
'memory_total_mb',
'memory_usage_percent',
'storage_block_device',
'storage_usage_mb',
'storage_total_mb',
'storage_usage_percent',
'undervoltage_detected',
];
if (isV14()) {
await this.outputData(device, outputFields, {
...options,
hideNullOrUndefinedValues: true,
titleField: 'device_name',
});
} else {
// Old output implementation
outputFields.unshift(`$${device.device_name}$`);
console.log(
getVisuals().table.vertical(
device,
outputFields.filter((f) => f !== 'device_name'),
),
);
}
}
}

View File

@ -23,40 +23,59 @@ import { applicationIdInfo } from '../../utils/messages';
import { runCommand } from '../../utils/helpers';
interface FlagsDef {
application?: string;
app?: string;
fleet?: string;
yes: boolean;
advanced: boolean;
'os-version'?: string;
drive?: string;
config?: string;
help: void;
'provisioning-key-name'?: string;
}
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
and writing it to an SD Card.
Register a new device in the selected fleet, download the OS image for the
fleet's default device type, configure the image and write it to an SD card.
This command effectively combines several other balena CLI commands in one,
namely:
Note, if the application option is omitted it will be prompted
for interactively.
'balena device register'
'balena os download'
'balena os build-config' or 'balena config generate'
'balena os configure'
'balena os local flash'
Possible arguments for the '--fleet', '--os-version' and '--drive' options can
be listed respectively with the commands:
'balena fleets'
'balena os versions'
'balena util available-drives'
If the '--fleet' or '--drive' options are omitted, interactive menus will be
presented with values to choose from. If the '--os-version' option is omitted,
the latest released OS version for the fleet's default device type will be used.
${applicationIdInfo.split('\n').join('\n\t\t')}
Image configuration questions will be asked interactively unless a pre-configured
'config.json' file is provided with the '--config' option. The file can be
generated with the 'balena config generate' or 'balena os build-config' commands.
`;
public static examples = [
'$ balena device init',
'$ balena device init --application MyApp',
'$ balena device init -a myorg/myapp',
'$ balena device init -f myorg/myfleet',
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes',
];
public static usage = 'device init';
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
app: cf.app,
fleet: cf.fleet,
yes: cf.yes,
advanced: flags.boolean({
char: 'v',
@ -75,6 +94,9 @@ export default class DeviceInitCmd extends Command {
config: flags.string({
description: 'path to the config JSON file, see `balena os build-config`',
}),
'provisioning-key-name': flags.string({
description: 'custom key name assigned to generated provisioning api key',
}),
help: cf.help,
};
@ -95,15 +117,13 @@ export default class DeviceInitCmd extends Command {
const logger = await Command.getLogger();
const balena = getBalenaSdk();
// Consolidate application options
options.application = options.application || options.app;
delete options.app;
// Get application and
const application = (await getApplication(
balena,
options['application'] ||
(await (await import('../../utils/patterns')).selectApplication()).id,
options.fleet ||
(
await (await import('../../utils/patterns')).selectApplication()
).id,
{
$expand: {
is_for__device_type: {
@ -115,7 +135,7 @@ export default class DeviceInitCmd extends Command {
// Register new device
const deviceUuid = balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.app_name}: ${deviceUuid}`);
console.info(`Registering to ${application.slug}: ${deviceUuid}`);
await balena.models.device.register(application.id, deviceUuid);
const device = await balena.models.device.get(deviceUuid);
@ -158,6 +178,13 @@ export default class DeviceInitCmd extends Command {
} else if (options.advanced) {
configureCommand.push('--advanced');
}
if (options['provisioning-key-name']) {
configureCommand.push(
'--provisioning-key-name',
options['provisioning-key-name'],
);
}
await runCommand(configureCommand);
}

View File

@ -15,22 +15,29 @@
* 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 { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
import { ExpectedError } from '../../errors';
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 +47,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 +59,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 +75,7 @@ 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,
fleet: cf.fleet,
help: cf.help,
};
@ -84,10 +89,7 @@ export default class DeviceMoveCmd extends Command {
const balena = getBalenaSdk();
const { tryAsInteger } = await import('../../utils/validation');
const { expandForAppName } = await import('../../utils/helpers');
options.application = options.application || options.app;
delete options.app;
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
// Parse ids string into array of correct types
const deviceIds: Array<string | number> = params.uuid
@ -98,15 +100,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';
@ -116,17 +119,15 @@ export default class DeviceMoveCmd extends Command {
const { getApplication } = await import('../../utils/sdk');
// Get destination application
const application = options.application
? await getApplication(balena, options.application)
const application = options.fleet
? await getApplication(balena, options.fleet)
: await this.interactivelySelectApplication(balena, devices);
// Move each device
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 +139,56 @@ export default class DeviceMoveCmd extends Command {
balena: BalenaSDK,
devices: ExtendedDevice[],
) {
const [deviceDeviceTypes, deviceTypes] = await Promise.all([
Promise.all(
devices.map((device) =>
balena.models.device.getManifestBySlug(
device.is_of__device_type[0].slug,
),
const { getExpandedProp } = await import('../../utils/pine');
// deduplicate the slugs
const deviceCpuArchs = Array.from(
new Set(
devices.map(
(d) => d.is_of__device_type[0].is_of__cpu_architecture[0].slug,
),
),
balena.models.config.getDeviceTypes(),
]);
);
const compatibleDeviceTypes = deviceTypes.filter((dt) =>
deviceDeviceTypes.every(
(deviceDeviceType) =>
balena.models.os.isArchitectureCompatibleWith(
deviceDeviceType.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceDeviceType.isDependent &&
dt.state !== 'DISCONTINUED',
),
const deviceTypeOptions = {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
} as const;
const deviceTypes = (await balena.models.deviceType.getAllSupported(
deviceTypeOptions,
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
const compatibleDeviceTypeSlugs = new Set(
deviceTypes
.filter((deviceType) => {
const deviceTypeArch = getExpandedProp(
deviceType.is_of__cpu_architecture,
'slug',
)!;
return deviceCpuArchs.every((deviceCpuArch) =>
balena.models.os.isArchitectureCompatibleWith(
deviceCpuArch,
deviceTypeArch,
),
);
})
.map((deviceType) => deviceType.slug),
);
const patterns = await import('../../utils/patterns');
try {
const application = await patterns.selectApplication(
(app) =>
compatibleDeviceTypes.some(
(dt) => dt.slug === app.is_for__device_type[0].slug,
) &&
// @ts-ignore using the extended device object prop
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
devices.some((device) => device.application_name !== app.app_name),
true,
);
return application;
} catch (err) {
if (deviceDeviceTypes.length) {
if (!compatibleDeviceTypeSlugs.size) {
throw new ExpectedError(
`${err.message}\nDo all devices have a compatible architecture?`,
);

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,27 +22,29 @@ import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
import type { Application } from 'balena-sdk';
import type { DataSetOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
application_name?: string | null;
fleet?: string | null; // 'org/name' slug
device_type?: string | null;
}
interface FlagsDef {
application?: string;
app?: string;
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
help: void;
json: boolean;
json?: 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,23 +52,19 @@ 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,
json: cf.json,
fleet: cf.fleet,
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
help: cf.help,
};
public static primary = true;
public static authenticated = true;
public async run() {
@ -74,15 +72,11 @@ export default class DevicesCmd extends Command {
const balena = getBalenaSdk();
// Consolidate application options
options.application = options.application || options.app;
delete options.app;
let devices;
if (options.application != null) {
if (options.fleet != null) {
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, options.application);
const application = await getApplication(balena, options.fleet);
devices = (await balena.models.device.getAllByApplication(
application.id,
expandForAppName,
@ -96,8 +90,9 @@ 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[];
device.application_name = belongsToApplication?.[0]?.app_name || null;
const belongsToApplication =
device.belongs_to__application as Application[];
device.fleet = belongsToApplication?.[0]?.slug || null;
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
@ -105,34 +100,52 @@ export default class DevicesCmd extends Command {
return device;
});
const fields = [
'id',
'uuid',
'device_name',
'device_type',
'application_name',
'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,
),
);
if (isV14()) {
const outputFields = [
'id',
'uuid',
'device_name',
'device_type',
'fleet',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
await this.outputData(devices, outputFields, {
...options,
displayNullValuesAs: 'N/a',
});
} else {
console.log(
getVisuals().table.horizontal(
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
fields,
),
);
// Old output implementation
const fields = [
'id',
'uuid',
'device_name',
'device_type',
'fleet',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
if (options.json) {
const { pickAndRename } = await import('../../utils/helpers');
const mapped = devices.map((device) => pickAndRename(device, fields));
console.log(JSON.stringify(mapped, null, 4));
} else {
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
fields,
),
);
}
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2019 Balena Ltd.
* Copyright 2016 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,19 +15,18 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type * as SDK from 'balena-sdk';
import * as _ from 'lodash';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
import type { DataSetOutputOptions } from '../../framework';
interface FlagsDef {
discontinued: boolean;
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
json?: boolean;
verbose?: boolean;
}
export default class DevicesSupportedCmd extends Command {
@ -36,11 +35,6 @@ export default class DevicesSupportedCmd extends Command {
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
The --verbose option adds extra columns/fields to the output, including the
"STATE" column whose values are one of 'new', 'released' or 'discontinued'.
However, 'discontinued' device types are only listed if the '--discontinued'
option is used.
The --json option is recommended when scripting the output of this command,
because the JSON format is less likely to change and it better represents data
types like lists and empty strings (for example, the ALIASES column contains a
@ -49,8 +43,7 @@ export default class DevicesSupportedCmd extends Command {
`;
public static examples = [
'$ balena devices supported',
'$ balena devices supported --verbose',
'$ balena devices supported -vj',
'$ balena devices supported --json',
];
public static usage = (
@ -59,61 +52,56 @@ export default class DevicesSupportedCmd extends Command {
).trim();
public static flags: flags.Input<FlagsDef> = {
discontinued: flags.boolean({
description: 'include "discontinued" device types',
}),
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
verbose: flags.boolean({
char: 'v',
description:
'add extra columns in the tabular output (ALIASES, ARCH, STATE)',
}),
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
};
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
const dts = await getBalenaSdk().models.config.getDeviceTypes();
let deviceTypes: Array<Partial<SDK.DeviceTypeJson.DeviceType>> = dts.map(
(d) => {
if (d.aliases && d.aliases.length) {
// remove aliases that are equal to the slug
d.aliases = d.aliases.filter((alias: string) => alias !== d.slug);
if (!options.json) {
// stringify the aliases array with commas and spaces
d.aliases = [d.aliases.join(', ')];
}
} else {
// ensure it is always an array (for the benefit of JSON output)
d.aliases = [];
}
return d;
},
);
if (!options.discontinued) {
deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED');
}
const fields = options.verbose
? ['slug', 'aliases', 'arch', 'state', 'name']
: ['slug', 'aliases', 'arch', 'name'];
deviceTypes = _.sortBy(
deviceTypes.map((d) => {
const picked = _.pick(d, fields);
// 'BETA' renamed to 'NEW'
picked.state = picked.state === 'BETA' ? 'NEW' : picked.state;
return picked;
const [dts, configDTs] = await Promise.all([
getBalenaSdk().models.deviceType.getAllSupported({
$expand: { is_of__cpu_architecture: { $select: 'slug' } },
$select: ['slug', 'name'],
}),
fields,
);
if (options.json) {
console.log(JSON.stringify(deviceTypes, null, 4));
getBalenaSdk().models.config.getDeviceTypes(),
]);
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
interface DT {
slug: string;
aliases: string[] | string;
arch: string;
name: string;
}
let deviceTypes: DT[] = [];
for (const slug of Object.keys(dtsBySlug)) {
const configDT: Partial<typeof configDTs[0]> =
configDTsBySlug[slug] || {};
const aliases = (configDT.aliases || []).filter(
(alias) => alias !== slug,
);
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
deviceTypes.push({
slug,
aliases: options.json ? aliases : aliases.join(', '),
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
name: dt.name || 'N/A',
});
}
const fields = ['slug', 'aliases', 'arch', 'name'];
deviceTypes = _.sortBy(deviceTypes, fields);
if (isV14()) {
await this.outputData(deviceTypes, fields, options);
} else {
const visuals = getVisuals();
const output = await visuals.table.horizontal(deviceTypes, fields);
console.log(output);
// Old output implementation
if (options.json) {
console.log(JSON.stringify(deviceTypes, null, 4));
} else {
const visuals = getVisuals();
const output = await visuals.table.horizontal(deviceTypes, fields);
console.log(output);
}
}
}
}

View File

@ -24,7 +24,7 @@ import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string; // device UUID
help: void;
quiet: boolean;
@ -38,18 +38,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 +60,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 +95,8 @@ 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'] },
fleet: { ...cf.fleet, exclusive: ['device'] },
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
quiet: cf.quiet,
service: cf.service,
@ -110,9 +108,9 @@ export default class EnvAddCmd extends Command {
);
const cmd = this;
if (!options.application && !options.device) {
if (!options.fleet && !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',
);
}
@ -152,16 +150,17 @@ export default class EnvAddCmd extends Command {
}
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.application) {
for (const app of options.application.split(',')) {
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
for (const app of options.fleet.split(',')) {
try {
await balena.models.application[varType].set(
app,
await getFleetSlug(balena, app),
params.name,
params.value,
);
} catch (err) {
console.error(`${err.message}, app: ${app}`);
console.error(`${err.message}, fleet: ${app}`);
process.exitCode = 1;
}
}
@ -183,15 +182,15 @@ 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,
params: ArgsDef,
options: FlagsDef,
) {
if (options.application) {
for (const app of options.application.split(',')) {
if (options.fleet) {
for (const app of options.fleet.split(',')) {
for (const service of options.service!.split(',')) {
try {
const serviceId = await getServiceIdForApp(sdk, app, service);
@ -201,7 +200,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 +215,7 @@ async function setServiceVars(
sdk,
uuid,
['id'],
['app_name'],
['slug'],
);
} catch (err) {
console.error(`${err.message}, device: ${uuid}`);
@ -225,11 +224,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 +257,7 @@ async function getServiceIdForApp(
}
if (serviceId === undefined) {
throw new ExpectedError(
`Cannot find service ${serviceName} for application ${appName}`,
`Cannot find service ${serviceName} for fleet ${appName}`,
);
}
return serviceId;

View File

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

View File

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

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2019 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,54 +22,55 @@ import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import { isV13 } from '../utils/version';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
application?: string;
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
config: boolean;
device?: string; // device UUID
json: boolean;
json?: boolean;
help: void;
service?: string; // service name
verbose: boolean;
}
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
appName?: string | null; // application name
fleet?: string | null; // fleet slug
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
interface DeviceServiceEnvironmentVariableInfo
extends SDK.DeviceServiceEnvironmentVariable {
appName?: string; // application name
fleet?: string; // fleet slug
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
interface ServiceEnvironmentVariableInfo
extends SDK.ServiceEnvironmentVariable {
appName?: string; // application name
fleet?: string; // fleet slug
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
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 +84,20 @@ 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')}
`;
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',
@ -108,44 +107,35 @@ export default class EnvsCmd extends Command {
public static usage = 'envs';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
all: flags.boolean({
default: false,
description: stripIndent`
No-op since balena CLI v12.0.0.`,
hidden: true,
}),
}),
application: { exclusive: ['device'], ...cf.application },
fleet: { ...cf.fleet, exclusive: ['device'] },
config: flags.boolean({
default: false,
char: 'c',
description: 'show configuration variables only',
exclusive: ['service'],
}),
device: { exclusive: ['application'], ...cf.device },
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
json: cf.json,
verbose: cf.verbose,
service: { exclusive: ['config'], ...cf.service },
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
service: { ...cf.service, exclusive: ['config'] },
};
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
const variables: EnvironmentVariableInfo[] = [];
await Command.checkLoggedIn();
if (!options.application && !options.device) {
throw new ExpectedError('You must specify an application or device');
if (!options.fleet && !options.device) {
throw new ExpectedError('Missing --fleet or --device option');
}
const balena = getBalenaSdk();
let appNameOrSlug = options.application;
let fleetSlug: string | undefined = options.fleet
? await (await import('../utils/sdk')).getFleetSlug(balena, options.fleet)
: undefined;
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
if (options.device) {
@ -154,27 +144,27 @@ export default class EnvsCmd extends Command {
balena,
options.device,
['uuid'],
['app_name'],
['slug'],
);
fullUUID = device.uuid;
if (app) {
appNameOrSlug = app.app_name;
fleetSlug = app.slug;
}
}
if (appNameOrSlug && options.service) {
await validateServiceName(balena, options.service, appNameOrSlug);
if (fleetSlug && options.service) {
await validateServiceName(balena, options.service, fleetSlug);
}
variables.push(...(await getAppVars(balena, appNameOrSlug, options)));
variables.push(...(await getAppVars(balena, fleetSlug, options)));
if (fullUUID) {
variables.push(
...(await getDeviceVars(balena, fullUUID, appNameOrSlug, options)),
...(await getDeviceVars(balena, fullUUID, fleetSlug, options)),
);
}
if (!options.json && variables.length === 0) {
const target =
(options.service ? `service "${options.service}" of ` : '') +
(options.application
? `application "${options.application}"`
(options.fleet
? `fleet "${options.fleet}"`
: `device "${options.device}"`);
throw new ExpectedError(`No environment variables found for ${target}`);
}
@ -186,33 +176,67 @@ export default class EnvsCmd extends Command {
varArray: EnvironmentVariableInfo[],
options: FlagsDef,
) {
const fields = ['id', 'name', 'value'];
const fields = ['id', 'name', 'value', 'fleet'];
// Replace undefined app names with 'N/A' or null
varArray = varArray.map((i: EnvironmentVariableInfo) => {
i.appName = i.appName || (options.json ? null : 'N/A');
i.fleet ||= options.json ? null : 'N/A';
return i;
});
fields.push(options.json ? 'appName' : 'appName => APPLICATION');
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
if (!options.config) {
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
}
if (isV14()) {
const results = [...varArray] as any;
if (options.json) {
this.log(
stringifyVarArray<SDK.EnvironmentVariableBase>(varArray, fields),
);
// Rename fields
if (options.device) {
if (options.json) {
fields.push('deviceUUID');
} else {
results.forEach((r: any) => {
r.device = r.deviceUUID;
delete r.deviceUUID;
});
fields.push('device');
}
}
if (!options.config) {
if (options.json) {
fields.push('serviceName');
} else {
results.forEach((r: any) => {
r.service = r.serviceName;
delete r.serviceName;
});
fields.push('service');
}
}
await this.outputData(results, fields, {
...options,
sort: options.sort || 'name',
});
} else {
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
fields,
),
);
// Old output implementation
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
if (!options.config) {
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
}
if (options.json) {
const { pickAndRename } = await import('../utils/helpers');
const mapped = varArray.map((o) => pickAndRename(o, fields));
this.log(JSON.stringify(mapped, null, 4));
} else {
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
fields,
),
);
}
}
}
}
@ -220,14 +244,14 @@ export default class EnvsCmd extends Command {
async function validateServiceName(
sdk: SDK.BalenaSDK,
serviceName: string,
appName: string,
fleetSlug: string,
) {
const services = await sdk.models.service.getAllByApplication(appName, {
const services = await sdk.models.service.getAllByApplication(fleetSlug, {
$filter: { service_name: serviceName },
});
if (services.length === 0) {
throw new ExpectedError(
`Service "${serviceName}" not found for application "${appName}"`,
`Service "${serviceName}" not found for fleet "${fleetSlug}"`,
);
}
}
@ -241,17 +265,17 @@ async function validateServiceName(
*/
async function getAppVars(
sdk: SDK.BalenaSDK,
appNameOrSlug: string | undefined,
fleetSlug: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const appVars: EnvironmentVariableInfo[] = [];
if (!appNameOrSlug) {
if (!fleetSlug) {
return appVars;
}
const vars = await sdk.models.application[
options.config ? 'configVar' : 'envVar'
].getAllByApplication(appNameOrSlug);
fillInInfoFields(vars, appNameOrSlug);
].getAllByApplication(fleetSlug);
fillInInfoFields(vars, fleetSlug);
appVars.push(...vars);
if (!options.config) {
const pineOpts: SDK.PineOptions<SDK.ServiceEnvironmentVariable> = {
@ -267,10 +291,10 @@ async function getAppVars(
};
}
const serviceVars = await sdk.models.service.var.getAllByApplication(
appNameOrSlug,
fleetSlug,
pineOpts,
);
fillInInfoFields(serviceVars, appNameOrSlug);
fillInInfoFields(serviceVars, fleetSlug);
appVars.push(...serviceVars);
}
return appVars;
@ -283,7 +307,7 @@ async function getAppVars(
async function getDeviceVars(
sdk: SDK.BalenaSDK,
fullUUID: string,
appNameOrSlug: string | undefined,
fleetSlug: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const printedUUID = options.json ? fullUUID : options.device!;
@ -292,7 +316,7 @@ async function getDeviceVars(
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceConfigVars, appNameOrSlug, printedUUID);
fillInInfoFields(deviceConfigVars, fleetSlug, printedUUID);
deviceVars.push(...deviceConfigVars);
} else {
const pineOpts: SDK.PineOptions<SDK.DeviceServiceEnvironmentVariable> = {
@ -313,13 +337,13 @@ async function getDeviceVars(
fullUUID,
pineOpts,
);
fillInInfoFields(deviceServiceVars, appNameOrSlug, printedUUID);
fillInInfoFields(deviceServiceVars, fleetSlug, printedUUID);
deviceVars.push(...deviceServiceVars);
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceEnvVars, appNameOrSlug, printedUUID);
fillInInfoFields(deviceEnvVars, fleetSlug, printedUUID);
deviceVars.push(...deviceEnvVars);
}
return deviceVars;
@ -335,7 +359,7 @@ function fillInInfoFields(
| EnvironmentVariableInfo[]
| DeviceServiceEnvironmentVariableInfo[]
| ServiceEnvironmentVariableInfo[],
appNameOrSlug?: string,
fleetSlug?: string,
deviceUUID?: string,
) {
for (const envVar of varArray) {
@ -344,33 +368,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.fleet = fleetSlug;
envVar.serviceName = envVar.serviceName || '*';
envVar.deviceUUID = deviceUUID || '*';
}
}
/**
* Transform each object (item) of varArray to preserve only the
* fields (keys) listed in the fields argument.
*/
function stringifyVarArray<T = Dictionary<any>>(
varArray: T[],
fields: string[],
): string {
const transformed = varArray.map((o: Dictionary<any>) =>
_.transform(
o,
(result, value, key) => {
if (fields.includes(key)) {
result[key] = value;
}
},
{} as Dictionary<any>,
),
);
return JSON.stringify(transformed, null, 4);
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 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,12 @@
*/
import { flags } from '@oclif/command';
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';
interface FlagsDef {
organization?: string;
@ -32,18 +33,18 @@ interface ArgsDef {
name: string;
}
export default class AppCreateCmd extends Command {
export default 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,31 +56,30 @@ 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,
};
@ -88,7 +88,7 @@ export default class AppCreateCmd extends Command {
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
AppCreateCmd,
FleetCreateCmd,
);
// Ascertain device type
@ -112,12 +112,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}".`,
);
}
@ -125,11 +125,8 @@ export default class AppCreateCmd extends Command {
}
// Output
const { isV13 } = await import('../../utils/version');
console.log(
isV13()
? `Application created: slug "${application.slug}", device type "${deviceType}"`
: `Application created: ${application.slug} (${deviceType}, id ${application.id})`,
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
);
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,58 +15,63 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
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 { isV14 } from '../../utils/version';
import type { DataOutputOptions } from '../../framework';
interface FlagsDef {
interface FlagsDef extends DataOutputOptions {
help: void;
}
interface ArgsDef {
application: string;
fleet: string;
}
export default class AppCmd extends Command {
export default 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,
...(isV14() ? cf.dataOutputFlags : {}),
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppCmd);
const { args: params, flags: options } = 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;
@ -76,15 +81,23 @@ export default class AppCmd extends Command {
application.device_type = application.is_for__device_type[0].slug;
application.commit = application.should_be_running__release[0]?.commit;
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
console.log(`== ${application.app_name}`);
console.log(
getVisuals().table.vertical(application, [
'id',
'device_type',
'slug',
'commit',
]),
);
if (isV14()) {
await this.outputData(
application,
['app_name', 'id', 'device_type', 'slug', 'commit'],
options,
);
} else {
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
console.log(`== ${application.slug}`);
console.log(
getVisuals().table.vertical(application, [
'id',
'device_type',
'slug',
'commit',
]),
);
}
}
}

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
@ -27,27 +28,27 @@ interface FlagsDef {
}
interface ArgsDef {
application: string;
fleet: string;
}
export default class AppPurgeCmd extends Command {
export default 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,
@ -56,7 +57,7 @@ export default class AppPurgeCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
const { getApplication } = await import('../../utils/sdk');
@ -64,7 +65,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);

View File

@ -15,28 +15,29 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
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';
interface FlagsDef {
help: void;
}
interface ArgsDef {
application: string;
fleet: string;
newName?: string;
}
export default class AppRenameCmd extends Command {
export default 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 +46,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,
@ -67,7 +68,7 @@ export default class AppRenameCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
const { validateApplicationName } = await import('../../utils/validation');
const { ExpectedError } = await import('../../errors');
@ -76,7 +77,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 +87,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,20 +102,31 @@ 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,
})) ||
'';
// Check they haven't used slug in new name
if (newName.includes('/')) {
throw new ExpectedError(
`New fleet name cannot include '/', please check that you are not specifying fleet slug.`,
);
}
// Rename
try {
await balena.models.application.rename(application.id, newName);
} catch (e) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
if ((e.message || '').toLowerCase().includes('unique')) {
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
}
// BalenaRequestError: Request error: App name may only contain [a-zA-Z0-9_-].
if ((e.message || '').toLowerCase().includes('name may only contain')) {
throw new ExpectedError(
`Error: application ${params.application} already exists.`,
`Error: new fleet name may only include characters [a-zA-Z0-9_-].`,
);
}
throw e;
@ -128,7 +138,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}`);

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
@ -27,26 +28,26 @@ interface FlagsDef {
}
interface ArgsDef {
application: string;
fleet: string;
}
export default class AppRestartCmd extends Command {
export default 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,
@ -55,14 +56,14 @@ export default class AppRestartCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(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);
}

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
@ -28,14 +29,14 @@ interface FlagsDef {
}
interface ArgsDef {
application: string;
fleet: string;
}
export default class AppRmCmd extends Command {
export default 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 +44,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,
@ -61,7 +62,7 @@ export default class AppRmCmd extends Command {
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
AppRmCmd,
FleetRmCmd,
);
const { confirm } = await import('../../utils/patterns');
@ -71,11 +72,11 @@ 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);

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

@ -0,0 +1,108 @@
/**
* @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 { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { isV14 } from '../utils/version';
import type { DataSetOutputOptions } from '../framework';
interface ExtendedApplication extends ApplicationWithDeviceType {
device_count: number;
online_devices: number;
device_type?: string;
}
interface FlagsDef extends DataSetOutputOptions {
help: void;
verbose?: boolean;
}
export default class FleetsCmd extends Command {
public static description = stripIndent`
List all fleets.
List all your balena fleets.
For detailed information on a particular fleet, use
\`balena fleet <fleet>\`
`;
public static examples = ['$ balena fleets'];
public static usage = 'fleets';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(FleetsCmd);
const balena = getBalenaSdk();
// Get applications
const applications =
(await balena.models.application.getAllDirectlyAccessible({
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
})) as ExtendedApplication[];
// Add extended properties
applications.forEach((application) => {
application.device_count = application.owns__device?.length ?? 0;
application.online_devices =
application.owns__device?.filter((d) => d.is_online).length || 0;
application.device_type = application.is_for__device_type[0].slug;
});
if (isV14()) {
await this.outputData(
applications,
[
'id',
'app_name',
'slug',
'device_type',
'device_count',
'online_devices',
],
options,
);
} else {
console.log(
getVisuals().table.horizontal(applications, [
'id',
'app_name => NAME',
'slug',
'device_type',
'online_devices',
'device_count',
]),
);
}
}
}

View File

@ -63,6 +63,7 @@ export default class OsinitCmd extends Command {
public static hidden = true;
public static root = true;
public static offlineCompatible = true;
public async run() {
const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);

View File

@ -23,7 +23,7 @@ import { applicationIdInfo } from '../utils/messages';
import { parseAsLocalHostnameOrIp } from '../utils/validation';
interface FlagsDef {
application?: string;
fleet?: string;
pollInterval?: number;
help?: void;
}
@ -34,22 +34,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 +57,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 +75,7 @@ export default class JoinCmd extends Command {
public static usage = 'join [deviceIpOrHostname]';
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
fleet: cf.fleet,
pollInterval: flags.integer({
description: 'the interval in minutes to check for updates',
char: 'i',
@ -98,7 +98,7 @@ export default class JoinCmd extends Command {
logger,
sdk,
params.deviceIpOrHostname,
options.application,
options.fleet,
options.pollInterval,
);
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,10 +20,13 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
import type { DataOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
interface FlagsDef extends DataOutputOptions {
help: void;
}
@ -52,27 +55,52 @@ export default class KeyCmd extends Command {
public static usage = 'key <id>';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
KeyCmd,
);
const key = await getBalenaSdk().models.key.get(params.id);
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
};
if (isV14()) {
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
public_key: key.public_key,
};
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
if (!options.json) {
// Id is redundant, since user must have provided it in command call
this.printTitle(displayKey.name);
this.outputMessage(displayKey.public_key);
} else {
await this.outputData(
displayKey,
['id', 'name', 'public_key'],
options,
);
}
} else {
// Old output implementation
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
};
// Since the public key string is long, it might
// wrap to lines below, causing the table layout to break.
// See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key);
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
// Since the public key string is long, it might
// wrap to lines below, causing the table layout to break.
// See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2022 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,11 @@ 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 { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
@ -35,13 +38,14 @@ export default class KeysCmd extends Command {
public static usage = 'keys';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public async run() {
this.parse<FlagsDef, {}>(KeysCmd);
const { flags: options } = this.parse<FlagsDef, {}>(KeysCmd);
const keys = await getBalenaSdk().models.key.getAll();
@ -50,6 +54,12 @@ export default class KeysCmd extends Command {
return { id: k.id, name: k.title };
});
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
// Display
if (isV14()) {
await this.outputData(displayKeys, ['id', 'name'], options);
} else {
// Old output implementation
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
}
}
}

View File

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

View File

@ -56,62 +56,48 @@ export default class LocalConfigureCmd extends Command {
};
public static root = true;
public static offlineCompatible = true;
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!');
}
readonly BOOT_PARTITION = 1;
readonly CONNECTIONS_FOLDER = '/system-connections';
getConfigurationSchema(connectionFileName?: string) {
getConfigurationSchema(bootPartition?: number, connectionFileName?: string) {
connectionFileName ??= 'resin-wifi';
return {
mapper: [
@ -127,6 +113,12 @@ export default class LocalConfigureCmd extends Command {
},
domain: [['config_json', 'hostname']],
},
{
template: {
developmentMode: '{{developmentMode}}',
},
domain: [['config_json', 'developmentMode']],
},
{
template: {
wifi: {
@ -150,14 +142,14 @@ export default class LocalConfigureCmd extends Command {
path: this.CONNECTIONS_FOLDER.slice(1),
// Reconfix still uses the older resin-image-fs, so still needs an
// object-based partition definition.
partition: this.BOOT_PARTITION,
partition: bootPartition,
},
},
config_json: {
type: 'json',
location: {
path: 'config.json',
partition: this.BOOT_PARTITION,
partition: bootPartition,
},
},
},
@ -177,6 +169,13 @@ export default class LocalConfigureCmd extends Command {
name: 'networkKey',
default: data.networkKey,
},
{
message:
'Enable development mode? (Open ports and root access - Not for production!)',
type: 'confirm',
name: 'developmentMode',
default: false,
},
{
message: 'Do you want to set advanced settings?',
type: 'confirm',
@ -250,21 +249,20 @@ export default class LocalConfigureCmd extends Command {
async prepareConnectionFile(target: string) {
const _ = await import('lodash');
const imagefs = await import('balena-image-fs');
const { getBootPartition } = await import('balena-config-json');
const files = await imagefs.interact(
target,
this.BOOT_PARTITION,
async (_fs) => {
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
},
);
const bootPartition = await getBootPartition(target);
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
});
let connectionFileName;
if (_.includes(files, 'resin-wifi')) {
// The required file already exists, nothing to do
} else if (_.includes(files, 'resin-sample.ignore')) {
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
await imagefs.interact(target, this.BOOT_PARTITION, async (_fs) => {
await imagefs.interact(target, bootPartition, async (_fs) => {
const readFileAsync = promisify(_fs.readFile);
const writeFileAsync = promisify(_fs.writeFile);
const contents = await readFileAsync(
@ -287,14 +285,14 @@ export default class LocalConfigureCmd extends Command {
connectionFileName = 'resin-sample';
} else {
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
await imagefs.interact(target, this.BOOT_PARTITION, async (_fs) => {
await imagefs.interact(target, bootPartition, async (_fs) => {
return await promisify(_fs.writeFile)(
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
this.CONNECTION_FILE,
);
});
}
return await this.getConfigurationSchema(connectionFileName);
return await this.getConfigurationSchema(bootPartition, connectionFileName);
}
async removeHostname(schema: any) {

View File

@ -20,12 +20,7 @@ import type { BlockDevice } from 'etcher-sdk/build/source-destination';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import {
getChalk,
getCliForm,
getVisuals,
stripIndent,
} from '../../utils/lazy';
import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
yes: boolean;
@ -70,6 +65,8 @@ export default class LocalFlashCmd extends Command {
help: cf.help,
};
public static offlineCompatible = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
LocalFlashCmd,
@ -83,8 +80,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 +90,15 @@ export default class LocalFlashCmd extends Command {
}
}
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const drive = await this.getDrive(options);
const yes =
options.yes ||
(await getCliForm().ask({
message: 'This will erase the selected drive. Are you sure?',
type: 'confirm',
name: 'yes',
default: false,
}));
if (!yes) {
console.log(getChalk().red.bold('Aborted image flash'));
process.exit(0);
}
const { confirm } = await import('../../utils/patterns');
await confirm(
options.yes,
'This will erase the selected drive. Are you sure?',
);
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const file = new sourceDestination.File({
path: params.image,
});

View File

@ -187,7 +187,7 @@ ${messages.reachingOut}`);
if (loginType === 'register') {
const open = await import('open');
const signupUrl = `https://dashboard.${balenaUrl}/signup`;
open(signupUrl, { wait: false });
await open(signupUrl, { wait: false });
throw new ExpectedError(`Please sign up at ${signupUrl}`);
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2022 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,11 @@ 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 { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
@ -36,12 +39,13 @@ export default class OrgsCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
...(isV14() ? cf.dataSetOutputFlags : {}),
};
public static authenticated = true;
public async run() {
this.parse<FlagsDef, {}>(OrgsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(OrgsCmd);
const { getOwnOrganizations } = await import('../utils/sdk');
@ -49,8 +53,13 @@ export default class OrgsCmd extends Command {
const organizations = await getOwnOrganizations(getBalenaSdk());
// Display
console.log(
getVisuals().table.horizontal(organizations, ['name', 'handle']),
);
if (isV14()) {
await this.outputData(organizations, ['name', 'handle'], options);
} else {
// Old output implementation
console.log(
getVisuals().table.horizontal(organizations, ['name', 'handle']),
);
}
}
}

View File

@ -35,10 +35,10 @@ interface ArgsDef {
export default class OsBuildConfigCmd extends Command {
public static description = stripIndent`
Build an OS config and save it to a JSON file.
Prepare a configuration file for use by the 'os configure' command.
Interactively generate an OS config once, so that the generated config
file can be used in \`balena os configure\`, skipping the interactive part.
Interactively generate a configuration file that can then be used as
non-interactive input by the 'balena os configure' command.
`;
public static examples = [

View File

@ -23,27 +23,26 @@ 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, devModeInfo } from '../../utils/messages';
const BOOT_PARTITION = 1;
const CONNECTIONS_FOLDER = '/system-connections';
interface FlagsDef {
advanced?: boolean;
application?: string;
app?: string;
fleet?: string;
config?: string;
'config-app-update-poll-interval'?: number;
'config-network'?: string;
'config-wifi-key'?: string;
'config-wifi-ssid'?: string;
dev?: boolean; // balenaOS development variant
device?: string; // device UUID
'device-api-key'?: string;
'device-type'?: string;
help?: void;
version?: string;
'system-connection': string[];
'initial-device-name'?: string;
'provisioning-key-name'?: string;
}
interface ArgsDef {
@ -52,23 +51,21 @@ interface ArgsDef {
interface Answers {
appUpdatePollInterval: number; // in minutes
developmentMode?: boolean; // balenaOS development variant
deviceType: string; // e.g. "raspberrypi3"
network: 'ethernet' | 'wifi';
version: string; // e.g. "2.32.0+rev1"
wifiSsid?: string;
wifiKey?: string;
provisioningKeyName?: string;
}
const deviceApiKeyDeprecationMsg = stripIndent`
The --device-api-key option is deprecated and will be removed in a future release.
A suitable key is automatically generated or fetched if this option is omitted.`;
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:
@ -76,18 +73,18 @@ 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 is 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
${devModeInfo.split('\n').join('\n\t\t')}
The --system-connection (-c) option is used to inject NetworkManager connection
profiles for additional network interfaces, such as cellular/GSM or additional
WiFi or ethernet connections. This option may be passed multiple times in case there
are multiple files to inject. See connection profile examples and reference at:
https://www.balena.io/docs/reference/OS/network/2.x/
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
${applicationIdInfo.split('\n').join('\n\t\t')}
Note: This command is currently not supported on Windows natively. Windows users
@ -98,12 +95,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 = [
@ -122,15 +117,15 @@ 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'] },
fleet: { ...cf.fleet, exclusive: ['device'] },
config: flags.string({
description:
'path to a pre-generated config.json file to be injected in the OS image',
exclusive: ['provisioning-key-name'],
}),
'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)',
@ -142,15 +137,11 @@ 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-api-key': flags.string({
char: 'k',
description:
'custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)',
}),
dev: cf.dev,
device: { ...cf.device, exclusive: ['fleet', 'provisioning-key-name'] },
'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:
@ -166,16 +157,19 @@ export default class OsConfigureCmd extends Command {
description:
"paths to local files to place into the 'system-connections' directory",
}),
'provisioning-key-name': flags.string({
description: 'custom key name assigned to generated provisioning api key',
exclusive: ['config', 'device'],
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
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;
await validateOptions(options);
@ -202,7 +196,7 @@ export default class OsConfigureCmd extends Command {
};
deviceTypeSlug = device.is_of__device_type[0].slug;
} else {
app = (await getApplication(balena, options.application!, {
app = (await getApplication(balena, options.fleet!, {
$expand: {
is_for__device_type: { $select: 'slug' },
},
@ -223,25 +217,28 @@ export default class OsConfigureCmd extends Command {
configJson = JSON.parse(rawConfig);
}
const osVersion =
options.version ||
(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit));
const { validateDevOptionAndWarn } = await import('../../utils/config');
await validateDevOptionAndWarn(options.dev, osVersion);
const answers: Answers = await askQuestionsForDeviceType(
deviceTypeManifest,
options,
configJson,
);
if (options.application) {
if (options.fleet) {
answers.deviceType = deviceTypeSlug;
}
answers.version =
options.version ||
(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit));
answers.version = osVersion;
answers.developmentMode = options.dev;
answers.provisioningKeyName = options['provisioning-key-name'];
if (_.isEmpty(configJson)) {
if (device) {
configJson = await generateDeviceConfig(
device,
options['device-api-key'],
answers,
);
configJson = await generateDeviceConfig(device, undefined, answers);
} else {
configJson = await generateApplicationConfig(app!, answers);
}
@ -281,10 +278,13 @@ export default class OsConfigureCmd extends Command {
}),
);
const { getBootPartition } = await import('balena-config-json');
const bootPartition = await getBootPartition(params.image);
const imagefs = await import('balena-image-fs');
for (const { name, content } of files) {
await imagefs.interact(image, BOOT_PARTITION, async (_fs) => {
await imagefs.interact(image, bootPartition, async (_fs) => {
return await promisify(_fs.writeFile)(
path.join(CONNECTIONS_FOLDER, name),
content,
@ -299,23 +299,16 @@ export default class OsConfigureCmd extends Command {
async function validateOptions(options: FlagsDef) {
// The 'device' and 'application' options are declared "exclusive" in the oclif
// flag definitions above, so oclif will enforce that they are not both used together.
if (!options.device && !options.application) {
if (!options.device && !options.fleet) {
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']) {
if (!options.fleet && 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']) {
console.error(stripIndent`
-------------------------------------------------------------------------------------------
Warning: ${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t\t')}
-------------------------------------------------------------------------------------------
`);
}
await Command.checkLoggedIn();
}
@ -365,7 +358,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.fleet}`,
);
}
}
@ -390,7 +383,13 @@ async function askQuestionsForDeviceType(
options: FlagsDef,
configJson?: import('../../utils/config').ImgConfig,
): Promise<Answers> {
const answerSources: any[] = [camelifyConfigOptions(options)];
const answerSources: any[] = [
{
...camelifyConfigOptions(options),
app: options.fleet,
application: options.fleet,
},
];
const defaultAnswers: Partial<Answers> = {};
const questions: any = deviceType.options;
let extraOpts: { override: object } | undefined;

View File

@ -34,30 +34,33 @@ export default class OsDownloadCmd extends Command {
public static description = stripIndent`
Download an unconfigured OS image.
Download an unconfigured OS image for a certain device type.
Check available types with \`balena devices supported\`
Download an unconfigured OS image for the specified device type.
Check available device types with 'balena devices supported'.
Note: Currently this command only works with balenaCloud, not openBalena.
If using openBalena, please download the OS from: https://www.balena.io/os/
If version is not specified the newest stable (non-pre-release) version of OS
is downloaded (if available), otherwise the newest version (if all existing
versions for the given device type are pre-release).
The '--version' option is used to select the balenaOS version. If omitted,
the latest released version is downloaded (and if only pre-release versions
exist, the latest pre-release version is downloaded).
You can pass \`--version menu\` to pick the OS version from the interactive menu
of all available versions.
Use '--version menu' or '--version menu-esr' to interactively select the
OS version. The latter lists ESR versions which are only available for
download on Production and Enterprise plans. See also:
https://www.balena.io/docs/reference/OS/extended-support-release/
To download a development image append \`.dev\` to the version or select from
the interactive menu.
Development images can be selected by appending \`.dev\` to the version.
`;
public static examples = [
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2021.10.2.prod',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu-esr',
];
public static args = [
@ -78,11 +81,13 @@ export default class OsDownloadCmd extends Command {
}),
version: flags.string({
description: stripIndent`
exact version number, or a valid semver range,
version number (ESR or non-ESR versions),
or semver range (non-ESR versions only),
or 'latest' (includes pre-releases),
or 'default' (excludes pre-releases if at least one stable version is available),
or 'default' (excludes pre-releases if at least one released version is available),
or 'recommended' (excludes pre-releases, will fail if only pre-release versions are available),
or 'menu' (will show the interactive menu)
or 'menu' (interactive menu, non-ESR versions),
or 'menu-esr' (interactive menu, ESR versions)
`,
}),
help: cf.help,
@ -93,8 +98,49 @@ export default class OsDownloadCmd extends Command {
OsDownloadCmd,
);
// balenaOS ESR versions require user authentication
if (options.version) {
const { isESR } = await import('balena-image-manager');
if (options.version === 'menu-esr' || isESR(options.version)) {
try {
await OsDownloadCmd.checkLoggedIn();
} catch (e) {
const { ExpectedError, NotLoggedInError } = await import(
'../../errors'
);
if (e instanceof NotLoggedInError) {
throw new ExpectedError(stripIndent`
${e.message}
User authentication is required to download balenaOS ESR versions.`);
}
throw e;
}
}
}
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;
e.message ||= '';
if (
e.code === 'BalenaRequestError' ||
e.message.toLowerCase().includes('no such version')
) {
const version = options.version || '';
if (
!version.endsWith('.dev') &&
!version.endsWith('.prod') &&
/^v?\d+\.\d+\.\d+/.test(version)
) {
e.message += `
** Hint: some OS releases require specifying the full OS version including
** the '.prod' or '.dev' suffix, e.g. '--version 2021.10.2.prod'`;
}
}
throw e;
}
}
}

View File

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

View File

@ -18,9 +18,10 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { stripIndent } from '../../utils/lazy';
interface FlagsDef {
esr?: boolean;
help: void;
}
@ -34,6 +35,9 @@ export default class OsVersionsCmd extends Command {
Show the available balenaOS versions for the given device type.
Check available types with \`balena devices supported\`.
balenaOS ESR versions can be listed with the '--esr' option. See also:
https://www.balena.io/docs/reference/OS/extended-support-release/
`;
public static examples = ['$ balena os versions raspberrypi3'];
@ -50,18 +54,22 @@ export default class OsVersionsCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
esr: flags.boolean({
description: 'select balenaOS ESR versions',
default: false,
}),
};
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(OsVersionsCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
OsVersionsCmd,
);
const {
versions: vs,
recommended,
} = await getBalenaSdk().models.os.getSupportedVersions(params.type);
const { formatOsVersion, getOsVersions } = await import(
'../../utils/cloud'
);
const vs = await getOsVersions(params.type, !!options.esr);
vs.forEach((v) => {
console.log(`v${v}` + (v === recommended ? ' (recommended)' : ''));
});
console.log(vs.map((v) => formatOsVersion(v)).join('\n'));
}
}

View File

@ -15,8 +15,8 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import {
getBalenaSdk,
@ -27,20 +27,15 @@ import {
import { applicationIdInfo } 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 { 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 +51,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 +77,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 +93,15 @@ 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,
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 +112,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,
@ -169,6 +166,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}`,
@ -177,15 +182,9 @@ Can be repeated to add multiple certificates.\
// balena-preload currently does not work with numerical app IDs
// Load app here, and use app slug from hereon
if (options.app && !options.app.includes('/')) {
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../utils/sdk');
const application = await getApplication(balena, options.app);
if (!application) {
throw new ExpectedError(`Application not found: ${options.app}`);
}
options.app = application.slug;
}
const fleetSlug: string | undefined = options.fleet
? await (await import('../utils/sdk')).getFleetSlug(balena, options.fleet)
: undefined;
const progressBars: {
[key: string]: ReturnType<typeof getVisuals>['Progress'];
@ -221,17 +220,14 @@ Can be repeated to add multiple certificates.\
? 'latest'
: options.commit;
const image = params.image;
const appId = options.app;
const splashImage = options['splash-image'];
const additionalSpace = options['additional-space'];
const dontCheckArch = options['dont-check-arch'] || false;
const pinDevice = options['pin-device-to-release'] || false;
if (dontCheckArch && !appId) {
if (dontCheckArch && !fleetSlug) {
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.',
);
}
@ -248,7 +244,7 @@ Can be repeated to add multiple certificates.\
const preloader = new balenaPreload.Preloader(
null,
docker,
appId,
fleetSlug,
commit,
image,
splashImage,
@ -265,10 +261,17 @@ Can be repeated to add multiple certificates.\
if (signal) {
gotSignal = true;
nodeCleanup.uninstall(); // don't call cleanup handler again
preloader.cleanup().then(() => {
// calling process.exit() won't inform parent process of signal
process.kill(process.pid, signal);
});
preloader
.cleanup()
.then(() => {
// calling process.exit() won't inform parent process of signal
process.kill(process.pid, signal);
})
.catch((e) => {
if (process.env.DEBUG) {
console.error(e);
}
});
return false;
}
});
@ -285,7 +288,7 @@ Can be repeated to add multiple certificates.\
preloader.on('error', reject);
resolve(
this.prepareAndPreload(preloader, balena, {
appId,
appId: fleetSlug,
commit,
pinDevice,
}),
@ -308,7 +311,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 +324,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.getAllDirectlyAccessible({
$select: ['id', 'slug', '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: 'slug asc',
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
}
>;
}
async selectApplication(deviceTypeSlug: string) {
@ -409,14 +409,14 @@ 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,
name: app.slug,
value: app,
})),
});
@ -424,7 +424,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 +457,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,
@ -491,7 +491,7 @@ Would you like to disable automatic updates for this application now?\
});
}
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) {
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string) {
const { getApplication } = await import('../utils/sdk');
return (await getApplication(balenaSdk, appId, {
@ -523,7 +523,7 @@ Would you like to disable automatic updates for this application now?\
if (this.isCurrentCommit(options.commit)) {
if (!appCommit) {
throw new Error(
`Unexpected empty commit hash for app ID "${application.id}"`,
`Unexpected empty commit hash for fleet ID "${application.id}"`,
);
}
// handle `--commit current` (and its `--commit latest` synonym)

View File

@ -22,7 +22,6 @@ import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import type { BalenaSDK } from 'balena-sdk';
import { ExpectedError, instanceOf } from '../errors';
import { isV13 } from '../utils/version';
import { RegistrySecrets } from 'resin-multibuild';
import { lowercaseIfSlug } from '../utils/normalization';
import {
@ -36,7 +35,7 @@ enum BuildTarget {
}
interface ArgsDef {
applicationOrDevice: string;
fleetOrDevice: string;
}
interface FlagsDef {
@ -47,25 +46,23 @@ interface FlagsDef {
pull: boolean;
'noparent-check': boolean;
'registry-secrets'?: string;
gitignore?: boolean;
nogitignore?: boolean;
nolive: boolean;
detached: boolean;
service?: string[];
system: boolean;
env?: string[];
'convert-eol'?: boolean;
'noconvert-eol': boolean;
'multi-dockerignore': boolean;
'release-tag'?: string[];
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 +89,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 +112,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 +185,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,
}),
@ -217,16 +214,6 @@ export default class PushCmd extends Command {
`,
multiple: true,
}),
...(isV13()
? {}
: {
'convert-eol': flags.boolean({
description: 'No-op and deprecated since balena CLI v12.0.0',
char: 'l',
hidden: true,
default: false,
}),
}),
'noconvert-eol': flags.boolean({
description: `Don't convert line endings from CRLF (Windows format) to LF (Unix format).`,
default: false,
@ -236,37 +223,24 @@ export default class PushCmd extends Command {
'Have each service use its own .dockerignore file. See "balena help push".',
char: 'm',
default: false,
exclusive: ['gitignore'],
}),
...(isV13()
? {}
: {
nogitignore: flags.boolean({
description:
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
char: 'G',
hidden: true,
default: false,
}),
}),
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.`,
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 +266,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 +280,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 +336,13 @@ 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,
sdk,
opts,
};
@ -398,11 +368,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 +386,6 @@ export default class PushCmd extends Command {
multiDockerignore: options['multi-dockerignore'],
nocache: options.nocache,
pull: options.pull,
nogitignore: !options.gitignore,
noParentCheck: options['noparent-check'],
nolive: options.nolive,
detached: options.detached,

View File

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

View File

@ -0,0 +1,146 @@
/**
* @license
* Copyright 2016 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';
import type { DataOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataOutputOptions {
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',
exclusive: ['json', 'fields'],
}),
...(isV14() ? cf.dataOutputFlags : {}),
};
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,
);
if (options.composition) {
await this.showComposition(params.commitOrId);
} else {
await this.showReleaseInfo(params.commitOrId, options);
}
}
async showComposition(commitOrId: string | number) {
const release = await getBalenaSdk().models.release.get(commitOrId, {
$select: 'composition',
});
if (isV14()) {
this.outputMessage(jsyaml.dump(release.composition));
} else {
// Old output implementation
console.log(jsyaml.dump(release.composition));
}
}
async showReleaseInfo(commitOrId: string | number, options: FlagsDef) {
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
'build_log',
'start_timestamp',
'end_timestamp',
];
const release = await getBalenaSdk().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');
if (isV14()) {
await this.outputData(
{
tags: tagStr,
...release,
},
fields,
{
displayNullValuesAs: 'N/a',
...options,
},
);
} else {
// Old output implementation
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']));
}
}
}

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

@ -0,0 +1,101 @@
/**
* @license
* Copyright 2016 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';
import type { DataSetOutputOptions } from '../framework';
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
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> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public static args = [
{
name: 'fleet',
description: 'fleet name or slug (preferred)',
required: true,
},
];
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ReleasesCmd,
);
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
];
const balena = getBalenaSdk();
const { getFleetSlug } = await import('../utils/sdk');
const releases = await balena.models.release.getAllByApplication(
await getFleetSlug(balena, params.fleet),
{ $select: fields },
);
if (isV14()) {
await this.outputData(releases, fields, {
displayNullValuesAs: 'N/a',
...options,
});
} else {
// Old output implementation
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
fields,
),
);
}
}
}

View File

@ -68,6 +68,7 @@ export default class ScanCmd extends Command {
public static primary = true;
public static root = true;
public static offlineCompatible = true;
public async run() {
const _ = await import('lodash');
@ -87,9 +88,8 @@ export default class ScanCmd extends Command {
const ux = getCliUx();
ux.action.start('Scanning for local balenaOS devices');
const localDevices: LocalBalenaOsDevice[] = await discover.discoverLocalBalenaOsDevices(
discoverTimeout,
);
const localDevices: LocalBalenaOsDevice[] =
await discover.discoverLocalBalenaOsDevices(discoverTimeout);
const engineReachableDevices: boolean[] = await Promise.all(
localDevices.map(async ({ address }: { address: string }) => {
const docker = await dockerUtils.createClient({

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import type { DataOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataOutputOptions {
help: void;
}
@ -35,15 +38,27 @@ export default class SettingsCmd extends Command {
public static usage = 'settings';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public async run() {
this.parse<FlagsDef, {}>(SettingsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(SettingsCmd);
const settings = await getBalenaSdk().settings.getAll();
const prettyjson = await import('prettyjson');
console.log(prettyjson.render(settings));
if (isV14()) {
// Select all available fields for display
const fields = Object.keys(settings);
await this.outputData(settings, fields, {
noCapitalizeKeys: true,
...options,
});
} else {
// Old output implementation
const prettyjson = await import('prettyjson');
console.log(prettyjson.render(settings));
}
}
}

View File

@ -31,20 +31,20 @@ interface FlagsDef {
}
interface ArgsDef {
applicationOrDevice: string;
fleetOrDevice: string;
service?: string;
}
export default class SshCmd extends Command {
public static description = stripIndent`
SSH into the host or application container of a device.
Open a SSH prompt on a device's host OS or service container.
Start a shell on a local or remote device. If a service name is not provided,
a shell will be opened on the host OS.
If an application is provided, an interactive menu will be presented
for the selection of an online device. A shell will then be opened for the
host OS or service container of the chosen device.
If a fleet is provided, an interactive menu will be presented for the selection
of an online device. A shell will then be opened for the host OS or service
container of the chosen device.
For local devices, the IP address and .local domain name are supported.
If the device is referenced by IP or \`.local\` address, the connection
@ -64,7 +64,7 @@ export default class SshCmd extends Command {
`;
public static examples = [
'$ balena ssh MyApp',
'$ balena ssh MyFleet',
'$ balena ssh f49cefd',
'$ balena ssh f49cefd my-service',
'$ balena ssh f49cefd --port <port>',
@ -76,9 +76,9 @@ export default class SshCmd extends Command {
public static args = [
{
name: 'applicationOrDevice',
name: 'fleetOrDevice',
description:
'application name/slug/id, device uuid, or address of local device',
'fleet name/slug/id, device uuid, or address of local device',
required: true,
},
{
@ -88,7 +88,7 @@ export default class SshCmd extends Command {
},
];
public static usage = 'ssh <applicationOrDevice> [service]';
public static usage = 'ssh <fleetOrDevice> [service]';
public static flags: flags.Input<FlagsDef> = {
port: flags.integer({
@ -117,6 +117,7 @@ export default class SshCmd extends Command {
};
public static primary = true;
public static offlineCompatible = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
@ -124,10 +125,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 +137,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();
@ -144,10 +145,11 @@ export default class SshCmd extends Command {
const useProxy = !!proxyConfig && !options.noproxy;
// this will be a tunnelled SSH connection...
await Command.checkNotUsingOfflineMode();
await Command.checkLoggedIn();
const deviceUuid = await getOnlineTargetDeviceUuid(
sdk,
params.applicationOrDevice,
params.fleetOrDevice,
);
const device = await sdk.models.device.get(deviceUuid, {
@ -156,6 +158,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 +304,7 @@ export default class SshCmd extends Command {
// container
const childProcess = await import('child_process');
const { escapeRegExp } = await import('lodash');
const { which } = await import('../utils/helpers');
const { which } = await import('../utils/which');
const { deviceContainerEngineBinary } = await import(
'../utils/device/ssh'
);

View File

@ -23,7 +23,7 @@ import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string;
duration?: string;
help: void;
@ -35,16 +35,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 +52,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 +71,10 @@ export default class SupportCmd extends Command {
description: 'comma-separated list (no spaces) of device UUIDs',
char: 'd',
}),
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 slugs (preferred)',
},
duration: flags.string({
description:
@ -97,10 +97,8 @@ export default class SupportCmd extends Command {
const enabling = params.action === 'enable';
// Validation
if (!options.device && !options.application) {
throw new ExpectedError(
'At least one device or application must be specified',
);
if (!options.device && !options.fleet) {
throw new ExpectedError('At least one device or fleet must be specified');
}
if (options.duration != null && !enabling) {
@ -115,7 +113,7 @@ export default class SupportCmd extends Command {
const expiryTs = Date.now() + this.parseDuration(duration);
const deviceUuids = options.device?.split(',') || [];
const appNames = options.application?.split(',') || [];
const appNames = options.fleet?.split(',') || [];
const enablingMessage = 'Enabling support access for';
const disablingMessage = 'Disabling support access for';
@ -132,14 +130,17 @@ export default class SupportCmd extends Command {
ux.action.stop();
}
const { getFleetSlug } = await import('../utils/sdk');
// Process applications
for (const appName of appNames) {
const slug = await getFleetSlug(balena, appName);
if (enabling) {
ux.action.start(`${enablingMessage} application ${appName}`);
await balena.models.application.grantSupportAccess(appName, expiryTs);
ux.action.start(`${enablingMessage} fleet ${slug}`);
await balena.models.application.grantSupportAccess(slug, expiryTs);
} else if (params.action === 'disable') {
ux.action.start(`${disablingMessage} application ${appName}`);
await balena.models.application.revokeSupportAccess(appName);
ux.action.start(`${disablingMessage} fleet ${slug}`);
await balena.models.application.revokeSupportAccess(slug);
}
ux.action.stop();
}

View File

@ -22,11 +22,10 @@ import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
interface ArgsDef {
@ -35,16 +34,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 +60,17 @@ 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'],
fleet: {
...cf.fleet,
exclusive: ['device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
exclusive: ['fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
exclusive: ['fleet', 'device'],
},
help: cf.help,
};
@ -87,24 +82,20 @@ export default class TagRmCmd extends Command {
TagRmCmd,
);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
if (!options.fleet && !options.device && !options.release) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(TagRmCmd.missingResourceMessage);
}
const { tryAsInteger } = await import('../../utils/validation');
if (options.application) {
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.remove(
await getTypedApplicationIdentifier(balena, options.application),
await getFleetSlug(balena, options.fleet),
params.tagKey,
);
}
@ -130,9 +121,9 @@ export default class TagRmCmd extends Command {
protected static missingResourceMessage = stripIndent`
To remove a resource tag, you must provide exactly one of:
* An application, with --application <appNameOrSlug>
* A device, with --device <uuid>
* A release, with --release <id or commit>
* A fleet, with --fleet <fleetNameOrSlug>
* A device, with --device <UUID>
* A release, with --release <ID or commit>
See the help page for examples:

View File

@ -22,11 +22,10 @@ import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
interface ArgsDef {
@ -36,9 +35,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 +47,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 +73,17 @@ 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'],
fleet: {
...cf.fleet,
exclusive: ['device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
exclusive: ['fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
exclusive: ['fleet', 'device'],
},
help: cf.help,
};
@ -100,14 +95,10 @@ export default class TagSetCmd extends Command {
TagSetCmd,
);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
if (!options.fleet && !options.device && !options.release) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(TagSetCmd.missingResourceMessage);
}
@ -116,10 +107,10 @@ export default class TagSetCmd extends Command {
const { tryAsInteger } = await import('../../utils/validation');
if (options.application) {
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.set(
await getTypedApplicationIdentifier(balena, options.application),
await getFleetSlug(balena, options.fleet),
params.tagKey,
params.value,
);
@ -151,9 +142,9 @@ export default class TagSetCmd extends Command {
protected static missingResourceMessage = stripIndent`
To set a resource tag, you must provide exactly one of:
* An application, with --application <appNameOrSlug>
* A device, with --device <uuid>
* A release, with --release <id or commit>
* A fleet, with --fleet <fleetNameOrSlug>
* A device, with --device <UUID>
* A release, with --release <ID or commit>
See the help page for examples:

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 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,28 +21,30 @@ import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import type { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
application?: string;
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
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,22 +53,19 @@ 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'],
fleet: {
...cf.fleet,
exclusive: ['device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
exclusive: ['fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
exclusive: ['fleet', 'device'],
},
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
@ -75,25 +74,21 @@ 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;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
if (!options.fleet && !options.device && !options.release) {
throw new ExpectedError(this.missingResourceMessage);
}
const { tryAsInteger } = await import('../utils/validation');
let tags;
let tags: ApplicationTag[] | DeviceTag[] | ReleaseTag[] = [];
if (options.application) {
const { getTypedApplicationIdentifier } = await import('../utils/sdk');
if (options.fleet) {
const { getFleetSlug } = await import('../utils/sdk');
tags = await balena.models.application.tags.getAllByApplication(
await getTypedApplicationIdentifier(balena, options.application),
await getFleetSlug(balena, options.fleet),
);
}
if (options.device) {
@ -113,17 +108,23 @@ export default class TagsCmd extends Command {
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
}
if (!tags || tags.length === 0) {
if (tags.length === 0 && !options.json) {
// TODO: Later change to output message
throw new ExpectedError('No tags found');
}
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
if (isV14()) {
await this.outputData(tags, ['tag_key', 'value'], options);
} else {
// Old output implementation
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
}
}
protected missingResourceMessage = stripIndent`
To list tags for a resource, you must provide exactly one of:
* An application, with --application <appNameOrSlug>
* A fleet, with --fleet <fleetNameOrSlug>
* A device, with --device <uuid>
* A release, with --release <id or commit>

View File

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

View File

@ -38,6 +38,8 @@ export default class UtilAvailableDrivesCmd extends Command {
help: cf.help,
};
public static offlineCompatible = true;
public async run() {
this.parse<FlagsDef, {}>(UtilAvailableDrivesCmd);

View File

@ -57,6 +57,8 @@ export default class VersionCmd extends Command {
public static usage = 'version';
public static offlineCompatible = true;
public static flags: flags.Input<FlagsDef> = {
all: flags.boolean({
default: false,

234
lib/deprecation.ts Normal file
View File

@ -0,0 +1,234 @@
/**
* @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,
timeout: 4000,
});
} catch (e) {
// 404 is expected if `version` hasn't been published yet
if (e.response?.statusCode !== 404) {
throw new Error(`Failed to query "${url}":\n${e}`);
}
}
// response.body looks like a package.json file, plus possibly a
// `versionist.publishedAt` field added by `github.com/product-os/versionist`
const publishedAt: string | undefined =
response?.body?.versionist?.publishedAt;
if (!publishedAt && process.env.DEBUG) {
console.error(`\
[debug] ${this.debugPrefix}: balena CLI next major version "${this.nextMajorVersion}" not released, \
or release date not available`);
}
return publishedAt; // ISO 8601, e.g. '2021-06-27T16:46:10.000Z'
}
/**
* Check if we already know (cached value) when the next major version
* was released. If we don't know, check how long ago the npm registry
* was last fetched, and fetch again if it has been longer than
* `majorVersionFetchIntervalDays`.
*/
public async checkForNewReleasesIfNeeded() {
if (process.env.BALENARC_UNSUPPORTED) {
return; // for the benefit of code testing
}
await this.init();
if (this.cachedTimestamps[this.nextMajorVersion]) {
// A cached value exists: no need to check the npm registry
return;
}
const lastFetched = new Date(this.cachedTimestamps.lastFetched).getTime();
const daysSinceLastFetch = (this.now - lastFetched) / this.msInDay;
if (daysSinceLastFetch < this.majorVersionFetchIntervalDays) {
if (process.env.DEBUG) {
// toFixed(5) results in a precision of ~1 second
const days = daysSinceLastFetch.toFixed(5);
console.error(`\
[debug] ${this.debugPrefix}: ${days} days since last npm registry query for next major version release date.
[debug] Will not query the registry again until at least ${this.majorVersionFetchIntervalDays} days have passed.`);
}
return;
}
if (process.env.DEBUG) {
console.error(`\
[debug] ${
this.debugPrefix
}: Cache miss for the balena CLI next major version release date.
[debug] Will query ${this.getNpmUrl(this.nextMajorVersion)}`);
}
try {
const publishedAt = await this.fetchPublishedTimestampForVersion(
this.nextMajorVersion,
);
if (publishedAt) {
this.cachedTimestamps[this.nextMajorVersion] = publishedAt;
}
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] ${this.debugPrefix}: ${e}`);
}
}
// Refresh `lastFetched` regardless of whether or not the request to the npm
// registry was successful. Will try again after `majorVersionFetchIntervalDays`.
this.cachedTimestamps.lastFetched = new Date(this.now).toISOString();
await this.storage.set(this.cacheFile, this.cachedTimestamps);
}
/**
* Use previously cached data (local cache only, fast execution) to check
* whether this version of the CLI is deprecated as per deprecation policy,
* in which case warn about it and conditionally throw an error.
*/
public async warnAndAbortIfDeprecated() {
if (process.env.BALENARC_UNSUPPORTED) {
return; // for the benefit of code testing
}
await this.init();
const nextMajorDateStr = this.cachedTimestamps[this.nextMajorVersion];
if (!nextMajorDateStr) {
return;
}
const nextMajorDate = new Date(nextMajorDateStr).getTime();
const daysElapsed = Math.trunc((this.now - nextMajorDate) / this.msInDay);
if (daysElapsed > this.expiryDays) {
const { ExpectedError } = await import('./errors');
throw new ExpectedError(this.getExpiryMsg(daysElapsed));
} else if (daysElapsed > this.deprecationDays && process.stderr.isTTY) {
console.error(this.getDeprecationMsg(daysElapsed));
}
}
/** Separate function for the benefit of code testing */
getDeprecationMsg(daysElapsed: number) {
const { warnify } =
require('./utils/messages') as typeof import('./utils/messages');
return warnify(`\
CLI version ${this.nextMajorVersion} was released ${daysElapsed} days ago: please upgrade.
This version of the balena CLI (${this.currentVersion}) will exit with an error
message after ${this.expiryDays} days from the release of version ${this.nextMajorVersion},
as per deprecation policy: https://git.io/JRHUW#deprecation-policy
The --unsupported flag may be used to bypass this deprecation check and
allow the CLI to keep working beyond the deprecation period. However,
note that the balenaCloud or openBalena backends may be updated in a way
that is no longer compatible with this version.`);
}
/** Separate function the benefit of code testing */
getExpiryMsg(daysElapsed: number) {
return `
This version of the balena CLI (${this.currentVersion}) has expired: please upgrade.
${daysElapsed} days have passed since the release of CLI version ${this.nextMajorVersion}.
See deprecation policy at: https://git.io/JRHUW#deprecation-policy
The --unsupported flag may be used to bypass this deprecation check and
continue using this version of the CLI. However, note that the balenaCloud
or openBalena backends may be updated in a way that is no longer compatible
with this CLI version.`;
}
}

View File

@ -31,6 +31,8 @@ export class NotLoggedInError extends ExpectedError {}
export class InsufficientPrivilegesError extends ExpectedError {}
export class NotAvailableInOfflineModeError extends ExpectedError {}
export class InvalidPortMappingError extends ExpectedError {
constructor(mapping: string) {
super(`'${mapping}' is not a valid port mapping.`);
@ -174,6 +176,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 +288,3 @@ export const printErrorMessage = function (message: string) {
export const printExpectedErrorMessage = function (message: string) {
console.error(`${message}\n`);
};
/**
* Print a friendly error message and exit the CLI with an error code, BYPASSING
* error reporting through Sentry.io's platform (raven.Raven.captureException).
* Note that lib/errors.ts provides top-level error handling code to catch any
* otherwise uncaught errors, AND to report them through Sentry.io. But many
* "expected" errors (say, a JSON parsing error in a file provided by the user)
* don't warrant reporting through Sentry.io. For such mundane errors, catch
* them and call this function.
*
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
* If a specific process exit code x must be set, use process.exitCode = x
*/
export function exitWithExpectedError(message: string | Error): never {
if (message instanceof Error) {
({ message } = message);
}
printErrorMessage(message);
process.exit(1);
}

View File

@ -14,19 +14,9 @@
* 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',
});
});
import { getBalenaSdk, stripIndent } from './utils/lazy';
interface CachedUsername {
token: string;
@ -34,12 +24,12 @@ interface CachedUsername {
}
/**
* 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,53 @@ 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, timeout: 4000 });
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] Event tracking error: ${e.message || e}`);
}
if (e instanceof got.TimeoutError) {
console.error(stripIndent`
Timeout submitting analytics event to balenaCloud/openBalena.
If you are using the balena CLI in an air-gapped environment with a filtered
internet connection, set the BALENARC_OFFLINE_MODE=1 environment variable
when using CLI commands that do not strictly require access to balenaCloud.
`);
}
// Note: You can simulate a timeout using non-routable address 10.0.0.0
}
}

112
lib/fast-boot.ts Normal file
View File

@ -0,0 +1,112 @@
/**
* @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.
*/
/**
* This module sets up the `fast-boot2` module, including testing whether
* we have permissions over the cache file before even attempting to load
* fast boot.
* DON'T IMPORT BALENA-CLI MODULES HERE, as this module is loaded directly
* from `bin/balena`, before the CLI's entrypoint in `lib/app.ts`.
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const stat = process.pkg ? fs.statSync : fs.promises.stat;
let fastBootStarted = false;
export async function start() {
if (fastBootStarted) {
return;
}
try {
await $start();
fastBootStarted = true;
} catch (e) {
if (process.env.DEBUG) {
console.error(`\
[debug] Unable to start 'fast-boot2':
[debug] ${(e.message || '').split('\n').join('\n[debug] ')}
[debug] The CLI should still work, but it will run a bit slower.`);
}
}
}
export function stop() {
if (fastBootStarted) {
require('fast-boot2').stop();
}
fastBootStarted = false;
}
async function $start() {
const dotBalena = process.platform === 'win32' ? '_balena' : '.balena';
// TODO: take into account `~/.balenarc.yml` or `./balenarc.yml`,
// without hurting performance at this early loading stage.
const dataDir = path.normalize(
process.env.BALENARC_DATA_DIRECTORY || path.join(os.homedir(), dotBalena),
);
// Consider that the CLI may be installed to a folder owned by root
// such as `/usr[/local]/lib/balena-cli`, while being executed by
// a regular user account.
const cacheFile = path.join(dataDir, 'cli-module-cache.json');
const root = path.join(__dirname, '..');
const [, pJson, pStat, nStat] = await Promise.all([
ensureCanWrite(dataDir, cacheFile),
import('../package.json'),
stat(path.join(root, 'package.json'), { bigint: true }),
stat(path.join(root, 'npm-shrinkwrap.json'), { bigint: true }),
]);
// Include timestamps to account for dev-time changes to node_modules
const cacheKiller = `${pJson.version}-${pStat.mtimeMs}-${nStat.mtimeMs}`;
require('fast-boot2').start({
cacheFile,
cacheKiller,
cacheScope: root,
});
}
/**
* Check that `file` has write permission. If so, return straight away.
* Throw an error if:
* - `file` exists but does have write permissions.
* - `file` does not exist and `dir` exists, but `dir` does not have
* write permissions.
* - `file` does not exist and `dir` does not exist, and an attempt
* to create `dir` failed.
*/
async function ensureCanWrite(dir: string, file: string) {
const { access, mkdir } = fs.promises;
try {
try {
await access(file, fs.constants.W_OK);
return;
} catch (e) {
// OK if file does not exist
if (e.code !== 'ENOENT') {
throw e;
}
}
// file does not exist; ensure that the directory is writable
await mkdir(dir, { recursive: true, mode: 0o755 });
await access(dir, fs.constants.W_OK);
} catch (e) {
throw new Error(`Unable to write file "${file}":\n${e.message}`);
}
}

19
lib/framework/index.ts Normal file
View File

@ -0,0 +1,19 @@
/*
Copyright 2020 Balena
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 type { DataOutputOptions, DataSetOutputOptions } from './output';
export { DataOutputOptions, DataSetOutputOptions };

246
lib/framework/output.ts Normal file
View File

@ -0,0 +1,246 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getCliUx, getChalk } from '../utils/lazy';
/**
* Used to extend FlagsDef for commands that output single-record data.
* Exposed to user in command options.
*/
export interface DataOutputOptions {
fields?: string;
json?: boolean;
}
/**
* Used to extend FlagsDef for commands that output multi-record data.
* Exposed to user in command options.
*/
export interface DataSetOutputOptions extends DataOutputOptions {
filter?: string;
'no-header'?: boolean;
'no-truncate'?: boolean;
sort?: string;
}
// Not exposed to user
export interface InternalOutputOptions {
displayNullValuesAs?: string;
hideNullOrUndefinedValues?: boolean;
titleField?: string;
noCapitalizeKeys?: boolean;
}
/**
* Output message to STDERR
*/
export function outputMessage(msg: string) {
// Messages go to STDERR
console.error(msg);
}
/**
* Output result data to STDOUT
* Supports:
* - arrays of items (displayed in a tabular way),
* - single items (displayed in a field per row format).
*
* @param data Array of data objects to output
* @param fields Array of fieldnames, specifying the fields and display order
* @param options Output options
*/
export async function outputData(
data: any[] | {},
fields: string[],
options: (DataOutputOptions | DataSetOutputOptions) & InternalOutputOptions,
) {
if (Array.isArray(data)) {
await outputDataSet(data, fields, options as DataSetOutputOptions);
} else {
await outputDataItem(data, fields, options as DataOutputOptions);
}
}
/**
* Wraps the cli.ux table implementation, to output tabular data
*
* @param data Array of data objects to output
* @param fields Array of fieldnames, specifying the fields and display order
* @param options Output options
*/
async function outputDataSet(
data: any[],
fields: string[],
options: DataSetOutputOptions & InternalOutputOptions,
) {
// Oclif expects fields to be specified in the format used in table headers (though lowercase)
// By replacing underscores with spaces here, we can support both header format and actual field name
// (e.g. as seen in json output).
options.fields = options.fields?.replace(/_/g, ' ');
options.filter = options.filter?.replace(/_/g, ' ');
options.sort = options.sort?.replace(/_/g, ' ');
if (!options.json) {
data = data.map((d) => {
return processNullValues(d, options);
});
}
getCliUx().table(
data,
// Convert fields array to column object keys
// that cli.ux expects. We can later add support
// for both formats if beneficial
fields.reduce((ac, a) => ({ ...ac, [a]: {} }), {}),
{
...options,
...(options.json
? {
output: 'json',
}
: {}),
columns: options.fields,
printLine,
},
);
}
/**
* Outputs a single data object (similar to `resin-cli-visuals table.vertical`),
* but supporting a subset of options from `cli-ux table` (--json and --fields)
*
* @param data Array of data objects to output
* @param fields Array of fieldnames, specifying the fields and display order
* @param options Output options
*/
async function outputDataItem(
data: any,
fields: string[],
options: DataOutputOptions & InternalOutputOptions,
) {
let outData: typeof data = {};
// Convert comma separated list of fields in `options.fields` to array of correct format.
// Note, user may have specified the true field name (e.g. `some_field`),
// or the format displayed in headers (e.g. `Some field`, case insensitive).
const userSelectedFields = options.fields?.split(',').map((f) => {
return f.toLowerCase().trim().replace(/ /g, '_');
});
// Order and filter the fields based on `fields` parameter and `options.fields`
(userSelectedFields || fields).forEach((fieldName) => {
if (fields.includes(fieldName)) {
outData[fieldName] = data[fieldName];
}
});
if (
(options.displayNullValuesAs || options.hideNullOrUndefinedValues) &&
!options.json
) {
outData = processNullValues(outData, options);
}
if (options.json) {
printLine(JSON.stringify(outData, undefined, 2));
} else {
// Find longest key, so we can align results
const longestKeyLength = getLongestObjectKeyLength(outData);
if (options.titleField) {
printTitle(data[options.titleField as keyof any[]], options);
}
// Output one field per line
for (let [k, v] of Object.entries(outData)) {
const shim = ' '.repeat(longestKeyLength - k.length);
let kDisplay = k.replace(/_/g, ' ');
// Start multiline values on the line below the field name
if (typeof v === 'string' && v.includes('\n')) {
v = `\n${v}`;
}
if (!options.noCapitalizeKeys) {
kDisplay = capitalize(kDisplay);
}
if (k !== options.titleField) {
printLine(` ${bold(kDisplay) + shim} : ${v}`);
}
}
}
}
/**
* Amend null/undefined values in data as per options:
* - options.displayNullValuesAs will replace the value with the specified string
* - options.hideNullOrUndefinedValues will remove the property from the data
*
* @param data The data object to process
* @param options Output options
*
* @returns a copy of the data with amended values.
*/
function processNullValues(data: any, options: InternalOutputOptions) {
const dataCopy = { ...data };
Object.entries(dataCopy).forEach(([k, v]) => {
if (v == null) {
if (options.displayNullValuesAs) {
dataCopy[k] = options.displayNullValuesAs;
} else if (options.hideNullOrUndefinedValues) {
delete dataCopy[k];
}
}
});
return dataCopy;
}
/**
* Print a title with underscore
*
* @param title The title string to print
* @param options Output options
*/
export function printTitle(
title: string,
options?: InternalOutputOptions & DataSetOutputOptions,
) {
if (!options?.['no-header']) {
printLine(` ${capitalize(bold(title))}`);
printLine(` ${bold('─'.repeat(title.length))}`);
}
}
function printLine(s: any) {
// Duplicating oclif cli-ux's default implementation here,
// but using this one explicitly for ease of testing
process.stdout.write(s + '\n');
}
function capitalize(s: string) {
return `${s[0].toUpperCase()}${s.slice(1)}`;
}
function bold(s: string) {
return getChalk().bold(s);
}
function getLongestObjectKeyLength(o: any): number {
return Math.max(0, ...Object.keys(o).map((k) => k.length));
}

View File

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

View File

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

View File

@ -14,8 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { stripIndent } from './utils/lazy';
import { exitWithExpectedError } from './errors';
export let unsupportedFlag = false;
export interface AppOptions {
// Prevent the default behavior of flushing stdout after running a command
@ -50,11 +50,16 @@ 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);
}
// support global --v-next flag
if (extractBooleanFlag(cmdSlice, '--v-next')) {
const { version } = await import('../package.json');
const { inc } = await import('semver');
process.env.BALENA_CLI_VERSION_OVERRIDE = inc(version, 'major') || '';
}
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
}
// Enable bluebird long stack traces when in debug mode, must be set
@ -87,11 +92,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 +117,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`.';
@ -122,6 +137,13 @@ export function checkDeletedCommand(argvSlice: string[]): void {
'local scan': [replaced, 'scan', 'v11.0.0'],
'local ssh': [replaced, 'ssh', 'v11.0.0'],
'local stop': [removed, stopAlternative, 'v11.0.0'],
app: [replaced, 'fleet', 'v13.0.0'],
apps: [replaced, 'fleets', 'v13.0.0'],
'app create': [replaced, 'fleet create', 'v13.0.0'],
'app purge': [replaced, 'fleet purge', 'v13.0.0'],
'app rename': [replaced, 'fleet rename', 'v13.0.0'],
'app restart': [replaced, 'fleet restart', 'v13.0.0'],
'app rm': [replaced, 'fleet rm', 'v13.0.0'],
};
let cmd: string | undefined;
if (argvSlice.length > 1) {

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