Compare commits

..

163 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
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
148 changed files with 13624 additions and 11548 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

View File

@ -3,5 +3,8 @@ module.exports = {
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

View File

@ -4,6 +4,636 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## 13.1.13 - 2022-02-10
* Drop unused awaitDevice utility function [Lucian Buzzo]
## 13.1.12 - 2022-02-09
<details>
<summary> preload: Stop using the deprecated /device-types/v1 API endpoints [Thodoris Greasidis] </summary>
> ### balena-preload-12.0.0 - 2022-01-27
>
> * Improve types [Thodoris Greasidis]
> * Stop relying on the /device-types/v1 endpoints [Thodoris Greasidis]
> * Bump TypeScript to v4.5 [Thodoris Greasidis]
>
> <details>
> <summary> Bump balena-sdk to v16 [Thodoris Greasidis] </summary>
>
>> #### balena-sdk-16.0.0 - 2021-11-28
>>
>> * **BREAKING**: Merge the hostApp model into the OS model [Thodoris Greasidis]
>> * **BREAKING** Drop os.getSupportedVersions() method in favor of hostapp.getAvailableOsVersions() [Thodoris Greasidis]
>> * os.getMaxSatisfyingVersion: Add optional param to choose OS line type [Thodoris Greasidis]
>> * os.getMaxSatisfyingVersion: Include ESR versions [Thodoris Greasidis]
>> * os.getMaxSatisfyingVersion: Switch to use hostApps [Thodoris Greasidis]
>> * hostapp.getAvailableOsVersions: Add single device type argument overload [Thodoris Greasidis]
>> * hostapp.getAllOsVersions: Add single device type argument overload [Thodoris Greasidis]
>> * models.hostapp: Add a getAvailableOsVersions() convenience method [Thodoris Greasidis]
>> * Support optional extra PineOptions in hostapp.getAllOsVersions() [Thodoris Greasidis]
>> * **BREAKING** Include invalidated versions in hostapp.getAllOsVersions() [Thodoris Greasidis]
>> * models/application: Add getDirectlyAccessible & getAllDirectlyAccessible [Thodoris Greasidis]
>> * application.get: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
>> * application.getAll: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
>> * **BREAKING** Change application.getAll to include public apps [Thodoris Greasidis]
>> * **BREAKING** Drop targeting/retrieving apps by name in favor of slugs [Thodoris Greasidis]
>> * Bump minimum supported Typescript to v4.5.2 [Thodoris Greasidis]
>> * **BREAKING**: Stop actively supporting node 10 [Thodoris Greasidis]
>> * **BREAKING** Drop application.getAllWithDeviceServiceDetails() [Thodoris Greasidis]
>> * **BREAKING** Change apiKey.getAll() to return all key variants [Thodoris Greasidis]
>> * types: Drop is_in_local_mode from the Device model [Thodoris Greasidis]
>> * types: Drop user__is_member_of__application in favor of the term form [Thodoris Greasidis]
>> * typings: Drop Subscription's discounts__plan_addon property [Thodoris Greasidis]
>> * typings: Stop extending the JWTUser type in the User model [Thodoris Greasidis]
>> * models/config: Change the BETA device type state to NEW [Thodoris Greasidis]
>> * typings: Drop the PineWithSelectOnGet type [Thodoris Greasidis]
>> * Remove my_application from the supported resources [Thodoris Greasidis]
>> * typings: Properly type some Device properties [Thodoris Greasidis]
>> * typings: Drop the DeviceWithImageInstalls type [Thodoris Greasidis]
>>
>> #### balena-sdk-15.59.2 - 2021-11-28
>>
>>
>> <details>
>> <summary> Update balena-request to 11.5.0 [Thodoris Greasidis] </summary>
>>
>>> ##### balena-request-11.5.0 - 2021-11-28
>>>
>>> * Convert tests to JavaScript and drop coffeescript [Thodoris Greasidis]
>>> * Fix the jsdoc generation [Thodoris Greasidis]
>>> * Convert to typescript and publish typings [Thodoris Greasidis]
>>>
>> </details>
>>
>>
>> #### balena-sdk-15.59.1 - 2021-11-28
>>
>> * Fix the typings of the Image contract field [Thodoris Greasidis]
>> * Fix the typings for the Release contract field [Thodoris Greasidis]
>>
>> #### balena-sdk-15.59.0 - 2021-11-24
>>
>> * Add release setIsInvalidated function [Matthew Yarmolinsky]
>>
>> #### balena-sdk-15.58.1 - 2021-11-17
>>
>> * Update typescript to 4.5.2 [Thodoris Greasidis]
>>
>> #### balena-sdk-15.58.0 - 2021-11-16
>>
>> * models/release: Add note() method [Thodoris Greasidis]
>> * typings: Add the release.invalidation_reason property [Thodoris Greasidis]
>> * typings: Add the release.note property [Thodoris Greasidis]
>>
>> #### balena-sdk-15.57.2 - 2021-11-15
>>
>> * tests/logs: Increase the wait time for retrieving the subscribed logs [Thodoris Greasidis]
>> * tests/logs: Refactor to async-await [Thodoris Greasidis]
>>
>> #### balena-sdk-15.57.1 - 2021-11-11
>>
>> * typings: Fix $filters for resources with non numeric ids [Thodoris Greasidis]
>> * typings: Add application.can_use__application_as_host ReverseNavigation [Thodoris Greasidis]
>> * Add missing apiKey.getDeviceApiKeysByDevice docs [Thodoris Greasidis]
>>
>> #### balena-sdk-15.57.0 - 2021-11-05
>>
>> * models/api-key: Change update() & revoke() to work with all key variants [Thodoris Greasidis]
>>
>> #### balena-sdk-15.56.0 - 2021-11-04
>>
>> * models/apiKey: Add getDeviceApiKeysByDevice() method [Thodoris Greasidis]
>>
>> #### balena-sdk-15.55.0 - 2021-11-01
>>
>> * typings: Add the release.raw_version property [Thodoris Greasidis]
>>
>> #### balena-sdk-15.54.2 - 2021-10-25
>>
>> * application/create: Rely on the hostApps for detecting discontinued DTs [Thodoris Greasidis]
>>
>> #### balena-sdk-15.54.1 - 2021-10-22
>>
>> * tests/device: Async-await conversions & abstraction on multi-field tests [Thodoris Greasidis]
>>
>> #### balena-sdk-15.54.0 - 2021-10-20
>>
>> * tests: Register devices in chunks of 10 to avoid uuid conflicts in node [Thodoris Greasidis]
>> * Add known issue check on release isReccomanded logic [JSReds]
>> * Add known_issue_list to hostApp.getOsVersions() [JSReds]
>>
>> #### balena-sdk-15.53.0 - 2021-10-07
>>
>> * Add support for batch device supervisor updates [Thodoris Greasidis]
>>
>> #### balena-sdk-15.52.0 - 2021-10-06
>>
>> * Add support for batch device pinning to release [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.4 - 2021-09-28
>>
>> * auth.isLoggedIn: Treat BalenaExpiredToken errors as logged out indicator [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.3 - 2021-09-28
>>
>> * Convert application spec to TypeScript [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.2 - 2021-09-28
>>
>> * application.trackLatestRelease: Fix using draft/invalidated releases [Thodoris Greasidis]
>> * application.isTrackingLatestRelease: Exclude draft&invalidated releases [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.1 - 2021-09-20
>>
>>
>> <details>
>> <summary> Update balena-request to v11.4.2 [Kyle Harding] </summary>
>>
>>> ##### balena-request-11.4.2 - 2021-09-20
>>>
>>> * Allow overriding the default zlib flush setting [Kyle Harding]
>>>
>> </details>
>>
>>
>> #### balena-sdk-15.51.0 - 2021-09-16
>>
>> * os.getConfig: Add typings for the provisioningKeyName option [Nitish Agarwal]
>>
>> #### balena-sdk-15.50.1 - 2021-09-13
>>
>> * models/os: Always first normalize the device type slug [Thodoris Greasidis]
>>
>> #### balena-sdk-15.50.0 - 2021-09-10
>>
>> * Add release.finalize to promote draft releases to final [toochevere]
>>
>> #### balena-sdk-15.49.1 - 2021-09-10
>>
>> * typings: Drop the v5-model-only application_type.is_host_os [Thodoris Greasidis]
>>
>> #### balena-sdk-15.49.0 - 2021-09-06
>>
>> * os.getSupportedOsUpdateVersions: Use the hostApp releases [Thodoris Greasidis]
>> * os.download: Use the hostApp for finding the latest release [Thodoris Greasidis]
>>
>> #### balena-sdk-15.48.3 - 2021-08-27
>>
>>
>> <details>
>> <summary> Update balena-request to 11.4.1 [Kyle Harding] </summary>
>>
>>> ##### balena-request-11.4.1 - 2021-08-27
>>>
>>> * Allow more lenient gzip decompression [Kyle Harding]
>>>
>> </details>
>>
>>
>> #### balena-sdk-15.48.2 - 2021-08-27
>>
>> * Improve hostapp.getAllOsVersions performance & reduce fetched data [Thodoris Greasidis]
>>
>> #### balena-sdk-15.48.1 - 2021-08-27
>>
>> * Update typescript to 4.4.2 [Thodoris Greasidis]
>>
>> #### balena-sdk-15.48.0 - 2021-08-15
>>
>> * Deprecate the release.release_version property [Thodoris Greasidis]
>> * typings: Add the release versioning properties [Thodoris Greasidis]
>>
>> #### balena-sdk-15.47.1 - 2021-08-10
>>
>> * Run browser tests using the minified browser bundle [Thodoris Greasidis]
>> * Move to uglify-js to fix const assignment bug in minified build [Thodoris Greasidis]
>>
>> #### balena-sdk-15.47.0 - 2021-08-09
>>
>> * typings: Add the release.is_final & is_finalized_at__date properties [Thodoris Greasidis]
>>
>> #### balena-sdk-15.46.1 - 2021-07-28
>>
>> * apiKey.getAll: Return only NamedUserApiKeys for backwards compatibility [Thodoris Greasidis]
>>
>> #### balena-sdk-15.46.0 - 2021-07-27
>>
>> * Add email verification & email request methods [Nitish Agarwal]
>>
>> #### balena-sdk-15.45.0 - 2021-07-26
>>
>> * Update generateProvisioningKey to include keyName [Nitish Agarwal]
>>
> </details>
>
>
</details>
## 13.1.11 - 2022-01-19
* chore: lib/auth/utils.ts: Replace deprecated url.resolve, use async/await [Paulo Castro]
* chore: Update @types/node to v12.20.42 [Paulo Castro]
## 13.1.10 - 2022-01-16
* Update docs and package.json re min Node.js supported version (12.8.0) [Paulo Castro]
## 13.1.9 - 2022-01-14
* Update packages in response to `colors` package issues [Scott Lowe]
## 13.1.8 - 2022-01-11
* local push: Fix "invalid character '/' looking for beginning of value" [Paulo Castro]
* v14 preparations: Fix TypeError produced by 'npx oclif manifest' [Paulo Castro]
## 13.1.7 - 2022-01-06
* Update to pkg 5 [Pagan Gazzard]
## 13.1.6 - 2022-01-04
* Automation: enforce noImplicitAny for the type-checked javascript [Pagan Gazzard]
## 13.1.5 - 2022-01-04
* Build: switch from using inline-source via gulp to using it directly [Pagan Gazzard]
## 13.1.4 - 2022-01-03
* Update pkg [Pagan Gazzard]
## 13.1.3 - 2022-01-03
* Convert lib/utils/deploy-legacy to typescript [Pagan Gazzard]
## 13.1.2 - 2022-01-03
* Convert lib/utils/compose to typescript [Pagan Gazzard]
## 13.1.1 - 2021-12-30
<details>
<summary> Update dependencies [Pagan Gazzard] </summary>
> ### docker-progress-5.0.1 - 2021-09-22
>
> * Fix for bad progress values from some registries causing a crash [Paul Jonathan Zoulin]
>
</details>
## 13.1.0 - 2021-12-29
* os configure, config generate: Add '--dev' option for OS developmentMode [Paulo Castro]
* local configure: Allow configuring 'developmentMode' in config.json [Paulo Castro]
* os build-config: Clarify command purpose in help output [Paulo Castro]
<details>
<summary> device os-update: Add support for unified dev/prod balenaOS releases [Paulo Castro] </summary>
> ### balena-sdk-16.9.0 - 2021-12-24
>
> * Support upgrading .dev to unified OS releases [Thodoris Greasidis]
>
> ### balena-sdk-16.8.2 - 2021-12-24
>
> * tests: Stop using mochainon [Thodoris Greasidis]
>
</details>
## 13.0.2 - 2021-12-24
* Update oclif [Pagan Gazzard]
## 13.0.1 - 2021-12-24
* os versions, os download: Replace deprecated version fields [Paulo Castro]
<details>
<summary> Update balena-sdk to v16.8.1 [Paulo Castro] </summary>
> ### balena-sdk-16.8.1 - 2021-12-23
>
> * logs: Stop using the querystring module [Thodoris Greasidis]
>
</details>
## 13.0.0 - 2021-12-23
* v13 RELEASE NOTES: see https://git.io/JDHxG [Paulo Castro]
* balena SDK v16: Ensure all SDK calls use fleet slug rather than name [Paulo Castro]
<details>
<summary> Update balena-sdk to v16.8.0 [Paulo Castro] </summary>
> ### balena-sdk-16.8.0 - 2021-12-22
>
> * os.getConfig: Accept additional developmentMode configuration option [Thodoris Greasidis]
>
> ### balena-sdk-16.7.0 - 2021-12-22
>
> * os.download: Fix the inferred method typings [Thodoris Greasidis]
> * os.download: Accept additional configuration options [Thodoris Greasidis]
>
> ### balena-sdk-16.6.0 - 2021-12-22
>
> * models.os: Use the native hostApp OS release version if it is set [Thodoris Greasidis]
> * models.os: Deprecate OsVersion.rawVersion in favor or raw_version [Thodoris Greasidis]
>
> ### balena-sdk-16.5.0 - 2021-12-22
>
> * os.getAllOsVersions: Add support for invariant OS releases [Thodoris Greasidis]
>
> ### balena-sdk-16.4.1 - 2021-12-21
>
> * os.getMaxSatisfyingVersion: Add ">" semver range tests [Thodoris Greasidis]
>
> ### balena-sdk-16.4.0 - 2021-12-21
>
> * os.getMaxSatisfyingVersion: Add support for ESR releases [Thodoris Greasidis]
>
> ### balena-sdk-16.3.0 - 2021-12-21
>
> * application.getAppByName: Add 'directly_accessible' convenience filter [Thodoris Greasidis]
>
> ### balena-sdk-16.2.3 - 2021-12-17
>
> * FIx the return type of config.getConfigVarSchema() [Thodoris Greasidis]
>
> ### balena-sdk-16.2.2 - 2021-12-17
>
> * os.getAvailableOsVersions: Exclude draft and non-successful releases [Thodoris Greasidis]
> * os.getAllOsVersions: Deprecate OsVersion.isRecommended [Thodoris Greasidis]
> * os.getAllOsVersions: Deprecate OsVersion.formattedVersion [Thodoris Greasidis]
>
> ### balena-sdk-16.2.1 - 2021-12-17
>
> * Drop require-npm4-to-publish [Thodoris Greasidis]
>
> ### balena-sdk-16.2.0 - 2021-12-17
>
> * minor: Add Configuration Variables Schema method [Vipul Gupta (@vipulgupta2048)]
>
> ### balena-sdk-16.1.0 - 2021-12-08
>
> * Add description field to generateProvisioningKey for apps. [Nitish Agarwal]
>
> ### balena-sdk-16.0.0 - 2021-11-28
>
> * **BREAKING**: Merge the hostApp model into the OS model [Thodoris Greasidis]
> * **BREAKING** Drop os.getSupportedVersions() method in favor of hostapp.getAvailableOsVersions() [Thodoris Greasidis]
> * os.getMaxSatisfyingVersion: Add optional param to choose OS line type [Thodoris Greasidis]
> * os.getMaxSatisfyingVersion: Include ESR versions [Thodoris Greasidis]
> * os.getMaxSatisfyingVersion: Switch to use hostApps [Thodoris Greasidis]
> * hostapp.getAvailableOsVersions: Add single device type argument overload [Thodoris Greasidis]
> * hostapp.getAllOsVersions: Add single device type argument overload [Thodoris Greasidis]
> * models.hostapp: Add a getAvailableOsVersions() convenience method [Thodoris Greasidis]
> * Support optional extra PineOptions in hostapp.getAllOsVersions() [Thodoris Greasidis]
> * **BREAKING** Include invalidated versions in hostapp.getAllOsVersions() [Thodoris Greasidis]
> * models/application: Add getDirectlyAccessible & getAllDirectlyAccessible [Thodoris Greasidis]
> * application.get: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
> * application.getAll: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
> * **BREAKING** Change application.getAll to include public apps [Thodoris Greasidis]
> * **BREAKING** Drop targeting/retrieving apps by name in favor of slugs [Thodoris Greasidis]
> * Bump minimum supported Typescript to v4.5.2 [Thodoris Greasidis]
> * **BREAKING**: Stop actively supporting node 10 [Thodoris Greasidis]
> * **BREAKING** Drop application.getAllWithDeviceServiceDetails() [Thodoris Greasidis]
> * **BREAKING** Change apiKey.getAll() to return all key variants [Thodoris Greasidis]
> * types: Drop is_in_local_mode from the Device model [Thodoris Greasidis]
> * types: Drop user__is_member_of__application in favor of the term form [Thodoris Greasidis]
> * typings: Drop Subscription's discounts__plan_addon property [Thodoris Greasidis]
> * typings: Stop extending the JWTUser type in the User model [Thodoris Greasidis]
> * models/config: Change the BETA device type state to NEW [Thodoris Greasidis]
> * typings: Drop the PineWithSelectOnGet type [Thodoris Greasidis]
> * Remove my_application from the supported resources [Thodoris Greasidis]
> * typings: Properly type some Device properties [Thodoris Greasidis]
> * typings: Drop the DeviceWithImageInstalls type [Thodoris Greasidis]
>
> ### balena-sdk-15.59.2 - 2021-11-28
>
>
> <details>
> <summary> Update balena-request to 11.5.0 [Thodoris Greasidis] </summary>
>
>> #### balena-request-11.5.0 - 2021-11-28
>>
>> * Convert tests to JavaScript and drop coffeescript [Thodoris Greasidis]
>> * Fix the jsdoc generation [Thodoris Greasidis]
>> * Convert to typescript and publish typings [Thodoris Greasidis]
>>
> </details>
>
>
> ### balena-sdk-15.59.1 - 2021-11-28
>
> * Fix the typings of the Image contract field [Thodoris Greasidis]
> * Fix the typings for the Release contract field [Thodoris Greasidis]
>
> ### balena-sdk-15.59.0 - 2021-11-24
>
> * Add release setIsInvalidated function [Matthew Yarmolinsky]
>
> ### balena-sdk-15.58.1 - 2021-11-17
>
> * Update typescript to 4.5.2 [Thodoris Greasidis]
>
> ### balena-sdk-15.58.0 - 2021-11-16
>
> * models/release: Add note() method [Thodoris Greasidis]
> * typings: Add the release.invalidation_reason property [Thodoris Greasidis]
> * typings: Add the release.note property [Thodoris Greasidis]
>
> ### balena-sdk-15.57.2 - 2021-11-15
>
> * tests/logs: Increase the wait time for retrieving the subscribed logs [Thodoris Greasidis]
> * tests/logs: Refactor to async-await [Thodoris Greasidis]
>
> ### balena-sdk-15.57.1 - 2021-11-11
>
> * typings: Fix $filters for resources with non numeric ids [Thodoris Greasidis]
> * typings: Add application.can_use__application_as_host ReverseNavigation [Thodoris Greasidis]
> * Add missing apiKey.getDeviceApiKeysByDevice docs [Thodoris Greasidis]
>
> ### balena-sdk-15.57.0 - 2021-11-05
>
> * models/api-key: Change update() & revoke() to work with all key variants [Thodoris Greasidis]
>
> ### balena-sdk-15.56.0 - 2021-11-04
>
> * models/apiKey: Add getDeviceApiKeysByDevice() method [Thodoris Greasidis]
>
> ### balena-sdk-15.55.0 - 2021-11-01
>
> * typings: Add the release.raw_version property [Thodoris Greasidis]
>
> ### balena-sdk-15.54.2 - 2021-10-25
>
> * application/create: Rely on the hostApps for detecting discontinued DTs [Thodoris Greasidis]
>
> ### balena-sdk-15.54.1 - 2021-10-22
>
> * tests/device: Async-await conversions & abstraction on multi-field tests [Thodoris Greasidis]
>
> ### balena-sdk-15.54.0 - 2021-10-20
>
> * tests: Register devices in chunks of 10 to avoid uuid conflicts in node [Thodoris Greasidis]
> * Add known issue check on release isReccomanded logic [JSReds]
> * Add known_issue_list to hostApp.getOsVersions() [JSReds]
>
> ### balena-sdk-15.53.0 - 2021-10-07
>
> * Add support for batch device supervisor updates [Thodoris Greasidis]
>
> ### balena-sdk-15.52.0 - 2021-10-06
>
> * Add support for batch device pinning to release [Thodoris Greasidis]
>
> ### balena-sdk-15.51.4 - 2021-09-28
>
> * auth.isLoggedIn: Treat BalenaExpiredToken errors as logged out indicator [Thodoris Greasidis]
>
> ### balena-sdk-15.51.3 - 2021-09-28
>
> * Convert application spec to TypeScript [Thodoris Greasidis]
>
> ### balena-sdk-15.51.2 - 2021-09-28
>
> * application.trackLatestRelease: Fix using draft/invalidated releases [Thodoris Greasidis]
> * application.isTrackingLatestRelease: Exclude draft&invalidated releases [Thodoris Greasidis]
>
</details>
* device, devices: Print the fleet's slug in 'org/fleetName' format [Paulo Castro]
* envs: Print the fleet's slug in 'org/fleetName' format [Paulo Castro]
* os configure: Remove deprecated '--device-api-key' option [Paulo Castro]
* Clean up unused v13 feature switch code [Paulo Castro]
* config read/write/inject/reconfigure: Place '--type' option behind v14 switch [Paulo Castro]
* fleet create: Don't print fleet's numeric database ID in confirmation msg [Paulo Castro]
* devices supported: Remove deprecated '--verbose' and '--discontinued' options [Paulo Castro]
* build/deploy/push: Remove deprecated '--convert-eol' option [Paulo Castro]
* Move some v13 features behind v14 switch. [Scott Lowe]
* Remove deprecated '--app' and '--application' options (renamed to '--fleet') [Paulo Castro]
* Remove deprecated commands 'app' and 'apps' (renamed to 'fleet' and 'fleets') [Paulo Castro]
* build/deploy/push: Remove deprecated '--[no]gitignore' option [Paulo Castro]
* v13 release: Flip the v13 feature switch [Paulo Castro]
* v13 release: Drop support for Node.js v10 (package.json engines.node) [Paulo Castro]
## 12.55.11 - 2021-12-23
* Update to typescript 4.5 [Pagan Gazzard]
## 12.55.10 - 2021-12-23
* Update dev dependencies [Pagan Gazzard]
## 12.55.9 - 2021-12-22
* os download: Future-proof '--version' format for unified dev/prod variants [Paulo Castro]
## 12.55.8 - 2021-12-21
* Include version info when installed on windows [Pagan Gazzard]
* Switch from the deprecated oclif-dev commands to the oclif commands [Pagan Gazzard]
## 12.55.7 - 2021-12-14
* push: Remove hardcoded 'balenaCloud' in console message [Pranav Peshwe]
## 12.55.6 - 2021-12-14
* Fix symbolic link regression in push & deploy [Thodoris Greasidis]
## 12.55.5 - 2021-12-13
* Drop unnecessary directory list created during balena deploy & push [Thodoris Greasidis]
## 12.55.4 - 2021-12-10
* os download, os versions: Accept device type aliases [Paulo Castro]
## 12.55.3 - 2021-12-09
* os download: Improve error message when not logged in (balenaOS ESR versions) [Paulo Castro]
* os download: Assume '.prod' suffix by default for all balenaOS versions [Paulo Castro]
## 12.55.2 - 2021-12-08
* v13 preparations: Standardize command data output [Scott Lowe]
## 12.55.1 - 2021-11-30
* chore: Bump multicast-dns to rebased commit [Kyle Harding]
## 12.55.0 - 2021-11-29
* Add provisioning key name option to config generate options [Nitish Agarwal]
## 12.54.5 - 2021-11-27
* os configure, local configure: Reuse disk partition scanning logic [Paulo Castro]
## 12.54.4 - 2021-11-26
* Bump 'balena-lint' and fix 'no-floating-promises' warnings [Paulo Castro]
## 12.54.3 - 2021-11-26
* Improve directory scan speed prior to tarballing [Lucian Buzzo]
## 12.54.2 - 2021-11-26
* Set the correct target state when using Compose "image" field [Lucian Buzzo]
## 12.54.1 - 2021-11-25
* Fix mistake in `config generate` examples [Scott Lowe]
## 12.54.0 - 2021-11-25
* Improve UX for offline usage [Scott Lowe]
## 12.53.2 - 2021-11-25
* Avoid creating an extra provisioning API key in os configure & config generate [Thodoris Greasidis]
## 12.53.1 - 2021-11-25
* Transitional changes to doc files for landr implementation [Scott Lowe]
## 12.53.0 - 2021-11-25
* config read/write/inject: Avoid need for internet access [Paulo Castro]
* config read: Add '--json' option for JSON output [Paulo Castro]
## 12.52.2 - 2021-11-24
* Delete 'doc/automated-init.md' and improve 'balena help device init' [Paulo Castro]
## 12.52.1 - 2021-11-22
* push/build: Add test cases for .dockerignore filtering corner cases [Paulo Castro]
## 12.52.0 - 2021-11-20
* os download: Display OS version actually downloaded (range or 'recommended') [Paulo Castro]
* os versions, os download: Add support for balenaOS ESR versions [Paulo Castro]
## 12.51.3 - 2021-11-16
* deploy: Ensure the release fails if an image's digest (hash) is missing [Paulo Castro]
## 12.51.2 - 2021-11-16
* Update balena CI configuration (remove Node v10 from npm pipeline list) [Paulo Castro]
## 12.51.1 - 2021-10-25
* Fix forums support link in README.md [Scott Lowe]
## 12.51.0 - 2021-10-22
* Add support for YAML anchors and aliases in 'docker-compose.yml' [dfunckt]

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.
@ -267,4 +267,3 @@ gotchas to bear in mind:
replace: `spec: 'tests/**/*.spec.ts',`
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`

View File

@ -78,7 +78,7 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
The npm installation involves building native (platform-specific) binary modules, which require
some development tools to be installed first, as follows.
> **The balena CLI currently requires Node.js version 10 (min 10.20.0) or 12.**
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
> **Versions 13 and later are not yet fully supported.**
### Install development tools

View File

@ -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/).

View File

@ -17,7 +17,7 @@
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';
@ -64,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 = {
@ -96,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'
@ -310,7 +311,7 @@ async function zipPkg() {
archive.on('warning', console.warn);
archive.pipe(outputStream);
archive.finalize();
archive.finalize().catch(reject);
});
}
@ -322,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')
@ -456,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.
@ -486,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

@ -36,19 +36,12 @@ const capitanoDoc = {
{
title: 'Fleet',
files: [
'build/commands/apps.js',
'build/commands/fleets.js',
'build/commands/app/index.js',
'build/commands/fleet/index.js',
'build/commands/app/create.js',
'build/commands/fleet/create.js',
'build/commands/app/purge.js',
'build/commands/fleet/purge.js',
'build/commands/app/rename.js',
'build/commands/fleet/rename.js',
'build/commands/app/restart.js',
'build/commands/fleet/restart.js',
'build/commands/app/rm.js',
'build/commands/fleet/rm.js',
],
},

View File

@ -105,4 +105,5 @@ async function printMarkdown() {
}
}
// 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);

View File

@ -103,4 +103,5 @@ export async function run(args?: string[]) {
}
}
// tslint:disable-next-line:no-floating-promises
run();

View File

@ -36,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);
});
};
@ -136,4 +136,5 @@ async function main() {
}
}
// tslint:disable-next-line:no-floating-promises
main();

View File

@ -117,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

@ -8,10 +8,9 @@ _balena() {
local context state line curcontext="$curcontext"
# Valid top-level completions
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util )
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 )
@ -44,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
;;

View File

@ -7,10 +7,9 @@ _balena_complete()
local cur prev
# Valid top-level completions
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util"
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"
@ -38,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) )
;;

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 --fleet FLEET_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. `FLEET_ID`. Create a fleet (`balena fleet create FLEET_NAME --type DEVICE_TYPE`) or find an existing one (`balena fleets`) and notice its ID.
1. `OS_VERSION`. Run
```bash
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

@ -77,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();
@ -91,7 +93,9 @@ 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. */
@ -149,7 +153,10 @@ async function oclifRun(command: string[], options: AppOptions) {
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
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

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

@ -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,181 +0,0 @@
/**
* @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 type { Output as ParserOutput } from '@oclif/parser';
import type { Application } from 'balena-sdk';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { appToFleetCmdMsg, warnify } from '../../utils/messages';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
export class FleetCreateCmd extends Command {
public static description = stripIndent`
Create a fleet.
Create a new balena fleet.
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 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.
Interactive dropdowns will be shown for selection if no device type or
organization is specified and there are multiple options to choose from.
If there is a single option to choose from, it will be chosen automatically.
This interactive behavior can be disabled by explicitly specifying a device
type and organization.
`;
public static examples = [
'$ 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: 'fleet name',
required: true,
},
];
public static usage = 'fleet create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the fleet should belong to',
}),
type: flags.string({
char: 't',
description:
'fleet device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params, flags: options } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCreateCmd);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
let application: Application;
try {
application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
});
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`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 fleets in organization "${organization}".`,
);
}
throw err;
}
// Output
const { isV13 } = await import('../../utils/version');
console.log(
isV13()
? `Fleet created: slug "${application.slug}", device type "${deviceType}"`
: `Fleet created: ${application.slug} (${deviceType}, id ${application.id})`,
);
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk());
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
}
}
export default class AppCreateCmd extends FleetCreateCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet create' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet create'
`;
public static examples = [];
public static usage = 'app create <name>';
public static args = FleetCreateCmd.args;
public static flags = FleetCreateCmd.flags;
public static authenticated = FleetCreateCmd.authenticated;
public static primary = FleetCreateCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCreateCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,124 +0,0 @@
/**
* @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 type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import type { Release } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export class FleetCmd extends Command {
public static description = stripIndent`
Display information about a single fleet.
Display detailed information about a single fleet.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet MyFleet',
'$ balena fleet myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public static primary = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCmd);
const { getApplication } = await import('../../utils/sdk');
const application = (await getApplication(getBalenaSdk(), params.fleet, {
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
},
})) as ApplicationWithDeviceType & {
should_be_running__release: [Release?];
// For display purposes:
device_type: string;
commit?: string;
};
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',
]),
);
}
}
export default class AppCmd extends FleetCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet'
`;
public static examples = [];
public static usage = 'app <fleet>';
public static args = FleetCmd.args;
public static flags = FleetCmd.flags;
public static authenticated = FleetCmd.authenticated;
public static primary = FleetCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,115 +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 type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export class FleetPurgeCmd extends Command {
public static description = stripIndent`
Purge data from a fleet.
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 fleet purge MyFleet',
'$ balena fleet purge myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet purge <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// balena.models.application.purge only accepts a numeric id
// so we must first fetch the app to get it's id,
const application = await getApplication(balena, params.fleet);
try {
await balena.models.application.purge(application.id);
} catch (e) {
if (e.message.toLowerCase().includes('no online device(s) found')) {
// application.purge throws an error if no devices are online
// ignore in this case.
} else {
throw e;
}
}
}
}
export default class AppPurgeCmd extends FleetPurgeCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet purge' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet purge'
`;
public static examples = [];
public static usage = 'app purge <fleet>';
public static args = FleetPurgeCmd.args;
public static flags = FleetPurgeCmd.flags;
public static authenticated = FleetPurgeCmd.authenticated;
public static primary = FleetPurgeCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,170 +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 type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import type { ApplicationType } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
newName?: string;
}
export class FleetRenameCmd extends Command {
public static description = stripIndent`
Rename a fleet.
Rename a fleet.
Note, if the \`newName\` parameter is omitted, it will be
prompted for interactively.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet rename OldName',
'$ balena fleet rename OldName NewName',
'$ balena fleet rename myorg/oldname NewName',
];
public static args = [
ca.fleetRequired,
{
name: 'newName',
description: 'the new name for the fleet',
},
];
public static usage = 'fleet rename <fleet> [newName]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
const { validateApplicationName } = await import('../../utils/validation');
const { ExpectedError } = await import('../../errors');
const balena = getBalenaSdk();
// 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.fleet, {
$expand: {
application_type: {
$select: ['is_legacy'],
},
},
});
// Check app exists
if (!application) {
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(
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
);
}
// Ascertain new name
const newName =
params.newName ||
(await getCliForm().ask({
message: 'Please enter the new name for this fleet:',
type: 'input',
validate: validateApplicationName,
})) ||
'';
// 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.`);
}
throw e;
}
// Get application again, to be sure of results
const renamedApplication = await balena.models.application.get(
application.id,
);
// Output result
console.log(`Fleet renamed`);
console.log('From:');
console.log(`\tname: ${application.app_name}`);
console.log(`\tslug: ${application.slug}`);
console.log('To:');
console.log(`\tname: ${renamedApplication.app_name}`);
console.log(`\tslug: ${renamedApplication.slug}`);
}
}
export default class AppRenameCmd extends FleetRenameCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet rename' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet rename'
`;
public static examples = [];
public static usage = 'app rename <fleet> [newName]';
public static args = FleetRenameCmd.args;
public static flags = FleetRenameCmd.flags;
public static authenticated = FleetRenameCmd.authenticated;
public static primary = FleetRenameCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

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

View File

@ -1,116 +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 type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
fleet: string;
}
export class FleetRmCmd extends Command {
public static description = stripIndent`
Remove a fleet.
Permanently remove a fleet.
The --yes option may be used to avoid interactive confirmation.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet rm MyFleet',
'$ balena fleet rm MyFleet --yes',
'$ balena fleet rm myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet rm <fleet>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params, flags: options } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRmCmd);
const { confirm } = await import('../../utils/patterns');
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// Confirm
await confirm(
options.yes ?? false,
`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.fleet);
// Remove
await balena.models.application.remove(application.id);
}
}
export default class AppRmCmd extends FleetRmCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet rm' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet rm'
`;
public static examples = [];
public static usage = 'app rm <fleet>';
public static args = FleetRmCmd.args;
public static flags = FleetRmCmd.flags;
public static authenticated = FleetRmCmd.authenticated;
public static primary = FleetRmCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRmCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,134 +0,0 @@
/**
* @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 type { Output as ParserOutput } from '@oclif/parser';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { appToFleetCmdMsg, warnify } from '../utils/messages';
interface ExtendedApplication extends ApplicationWithDeviceType {
device_count?: number;
online_devices?: number;
}
interface FlagsDef {
help: void;
verbose?: boolean;
}
export 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> = {
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;
protected useAppWord = false;
public async run(_parserOutput?: ParserOutput<FlagsDef, {}>) {
_parserOutput ||= this.parse<FlagsDef, {}>(FleetsCmd);
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',
this.useAppWord ? 'app_name' : 'app_name => NAME',
'slug',
'device_type',
'online_devices',
'device_count',
]),
);
}
}
const appsToFleetsRenameMsg = appToFleetCmdMsg
.replace(/'app'/g, "'apps'")
.replace(/'fleet'/g, "'fleets'");
export default class AppsCmd extends FleetsCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleets' command
${appsToFleetsRenameMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleets'
`;
public static examples = [];
public static usage = 'apps';
public static args = FleetsCmd.args;
public static flags = FleetsCmd.flags;
public static authenticated = FleetsCmd.authenticated;
public static primary = FleetsCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, {}>(AppsCmd);
if (process.stderr.isTTY) {
console.error(warnify(appsToFleetsRenameMsg));
}
this.useAppWord = true;
await super.run(parserOutput);
}
}

View File

@ -22,22 +22,18 @@ import * as cf from '../utils/common-flags';
import * as compose from '../utils/compose';
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
import {
appToFleetFlagMsg,
buildArgDeprecation,
dockerignoreHelp,
registrySecretsHelp,
warnify,
} from '../utils/messages';
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
import { buildProject, composeCliFlags } from '../utils/compose_ts';
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import { dockerCliFlags } from '../utils/docker';
import { isV13 } from '../utils/version';
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
arch?: string;
deviceType?: string;
application?: string;
fleet?: string;
source?: string; // Not part of command profile - source param copied here.
help: void;
@ -96,7 +92,6 @@ ${dockerignoreHelp}
description: 'the type of device this build is for',
char: 'd',
}),
...(isV13() ? {} : { application: cf.application }),
fleet: cf.fleet,
...composeCliFlags,
...dockerCliFlags,
@ -112,12 +107,7 @@ ${dockerignoreHelp}
BuildCmd,
);
if (options.application && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
await Command.checkLoggedInIf(!!options.application);
await Command.checkLoggedInIf(!!options.fleet);
(await import('events')).defaultMaxListeners = 1000;
@ -161,10 +151,8 @@ ${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(
@ -189,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;
@ -271,7 +259,6 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore,
});
}

View File

@ -19,19 +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,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
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;
@ -42,6 +36,7 @@ interface FlagsDef {
wifiSsid?: string;
wifiKey?: string;
appUpdatePollInterval?: string;
'provisioning-key-name'?: string;
help: void;
}
@ -53,6 +48,8 @@ export default class ConfigGenerateCmd extends Command {
The target balenaOS version must be specified with the --version option.
${devModeInfo.split('\n').join('\n\t\t')}
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.
@ -65,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 --fleet MyFleet --version 2.12.7',
'$ balena config generate --fleet myorg/myfleet --version 2.12.7',
'$ balena config generate --fleet MyFleet --version 2.12.7 --deviceType fincm3',
'$ balena config generate --fleet MyFleet --version 2.12.7 --output config.json',
'$ balena config generate --fleet MyFleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
'$ 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';
@ -81,20 +77,12 @@ export default class ConfigGenerateCmd extends Command {
description: 'a balenaOS version',
required: true,
}),
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device'],
},
app: { ...cf.app, exclusive: ['application', 'fleet', 'device'] },
appUpdatePollInterval: flags.string({
description: 'DEPRECATED alias for --updatePollInterval',
}),
}),
fleet: { ...cf.fleet, exclusive: ['application', 'app', 'device'] },
device: { ...cf.device, exclusive: ['application', 'app', 'fleet'] },
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+',
@ -126,7 +114,11 @@ export default class ConfigGenerateCmd extends Command {
}),
appUpdatePollInterval: flags.string({
description:
'supervisor cloud polling interval in minutes (e.g. for variable 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,
};
@ -165,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' },
},
@ -180,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,
);
@ -190,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 fleet ${options.application}`,
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
);
}
}
@ -199,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(
@ -244,19 +238,14 @@ export default class ConfigGenerateCmd extends Command {
protected async validateOptions(options: FlagsDef) {
const { ExpectedError } = await import('../../errors');
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
// Prefer options.application over options.app
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,18 +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 a 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.
Note: if using a private/custom device type, please ensure you are logged in
('balena login' command). Public device types do not require logging in.
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 = [
@ -57,12 +57,13 @@ 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 root = true;
public static offlineCompatible = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
@ -81,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,35 +21,40 @@ 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);
@ -61,9 +66,13 @@ export default class ConfigReadCmd extends Command {
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,38 +21,48 @@ 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() {
@ -65,10 +75,20 @@ export default class ConfigReconfigureCmd extends Command {
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,14 +64,13 @@ 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>(
@ -82,14 +84,14 @@ export default class ConfigWriteCmd extends Command {
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}`);
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
await denyMount(drive, async () => {
await safeUmount(drive);
await config.write(drive, options.type, configJSON);
await config.write(drive, '', configJSON);
});
console.info('Done');

View File

@ -319,7 +319,6 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore,
});
builtImagesByService = _.keyBy(builtImages, 'serviceName');

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,15 +21,15 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { appToFleetOutputMsg, warnify } from '../../utils/messages';
import { tryAsInteger } from '../../utils/validation';
import { isV13 } from '../../utils/version';
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;
@ -44,9 +44,8 @@ interface ExtendedDevice extends DeviceWithDeviceType {
undervoltage_detected?: boolean;
}
interface FlagsDef {
interface FlagsDef extends DataOutputOptions {
help: void;
v13: boolean;
}
interface ArgsDef {
@ -74,7 +73,7 @@ export default class DeviceCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
v13: cf.v13,
...(isV14() ? cf.dataOutputFlags : {}),
};
public static authenticated = true;
@ -84,7 +83,6 @@ export default class DeviceCmd extends Command {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceCmd,
);
const useAppWord = !options.v13 && !isV13();
const balena = getBalenaSdk();
@ -121,8 +119,8 @@ export default class DeviceCmd extends Command {
const belongsToApplication =
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
device.fleet = belongsToApplication?.[0]
? belongsToApplication[0].slug
: 'N/a';
device.device_type = device.is_of__device_type[0].slug;
@ -170,41 +168,52 @@ export default class DeviceCmd extends Command {
);
}
if (useAppWord && process.stderr.isTTY) {
console.error(warnify(appToFleetOutputMsg));
}
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',
];
console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
'id',
'device_type',
'status',
'is_online',
'ip_address',
'public_address',
'mac_address',
useAppWord ? 'application_name' : 'application_name => 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

@ -19,17 +19,10 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { applicationIdInfo } from '../../utils/messages';
import { runCommand } from '../../utils/helpers';
import { isV13 } from '../../utils/version';
interface FlagsDef {
application?: string;
app?: string;
fleet?: string;
yes: boolean;
advanced: boolean;
@ -37,35 +30,51 @@ interface FlagsDef {
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 the specified fleet
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:
If the --fleet 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 --fleet MyFleet',
'$ 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> = {
...(isV13()
? {}
: {
application: cf.application,
app: cf.app,
}),
fleet: cf.fleet,
yes: cf.yes,
advanced: flags.boolean({
@ -85,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,
};
@ -105,17 +117,10 @@ export default class DeviceInitCmd extends Command {
const logger = await Command.getLogger();
const balena = getBalenaSdk();
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
// Consolidate application options
options.application ||= options.app || options.fleet;
delete options.app;
// Get application and
const application = (await getApplication(
balena,
options['application'] ||
options.fleet ||
(
await (await import('../../utils/patterns')).selectApplication()
).id,
@ -130,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);
@ -173,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

@ -27,12 +27,7 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
type ExtendedDevice = PineTypedResult<
Device,
@ -42,8 +37,6 @@ type ExtendedDevice = PineTypedResult<
};
interface FlagsDef {
application?: string;
app?: string;
fleet?: string;
help: void;
}
@ -82,7 +75,6 @@ export default class DeviceMoveCmd extends Command {
public static usage = 'device move <uuid(s)>';
public static flags: flags.Input<FlagsDef> = {
...(isV13() ? {} : { app: cf.app, application: cf.application }),
fleet: cf.fleet,
help: cf.help,
};
@ -94,11 +86,6 @@ export default class DeviceMoveCmd extends Command {
DeviceMoveCmd,
);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
const balena = getBalenaSdk();
const { tryAsInteger } = await import('../../utils/validation');
@ -132,8 +119,8 @@ 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

View File

@ -75,7 +75,7 @@ export default class DeviceRegisterCmd extends Command {
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.
@ -20,30 +20,22 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
appToFleetOutputMsg,
jsonInfo,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
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;
v13: boolean;
json?: boolean;
}
export default class DevicesCmd extends Command {
@ -67,50 +59,24 @@ export default class DevicesCmd extends Command {
public static usage = 'devices';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'v13'],
},
app: { ...cf.app, exclusive: ['application', 'fleet', 'v13'] },
}),
fleet: { ...cf.fleet, exclusive: ['app', 'application'] },
json: cf.json,
fleet: cf.fleet,
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
help: cf.help,
v13: cf.v13,
};
public static primary = true;
public static authenticated = true;
protected useAppWord = false;
protected hasWarned = false;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
this.useAppWord = !options.fleet && !options.v13 && !isV13();
const balena = getBalenaSdk();
if (
(options.application || options.app) &&
!options.json &&
process.stderr.isTTY
) {
this.hasWarned = true;
console.error(warnify(appToFleetFlagMsg));
}
// Consolidate application options
options.application ||= options.app || options.fleet;
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,
@ -126,7 +92,7 @@ export default class DevicesCmd extends Command {
const belongsToApplication =
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]?.app_name || null;
device.fleet = belongsToApplication?.[0]?.slug || null;
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
@ -134,38 +100,52 @@ export default class DevicesCmd extends Command {
return device;
});
const jName = this.useAppWord ? 'application_name' : 'fleet_name';
const tName = this.useAppWord ? 'APPLICATION NAME' : 'FLEET';
const fields = [
'id',
'uuid',
'device_name',
'device_type',
options.json
? `application_name => ${jName}`
: `application_name => ${tName}`,
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
if (isV14()) {
const outputFields = [
'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));
await this.outputData(devices, outputFields, {
...options,
displayNullValuesAs: 'N/a',
});
} else {
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
console.error(warnify(appToFleetOutputMsg));
// 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,
),
);
}
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-2021 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.
@ -17,37 +17,24 @@
import { flags } from '@oclif/command';
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 { isV13 } from '../../utils/version';
import type { DataSetOutputOptions } from '../../framework';
interface FlagsDef {
discontinued: boolean;
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
json?: boolean;
verbose?: boolean;
}
const deprecatedInfo = isV13()
? ''
: `
The --verbose option may add extra columns/fields to the output. Currently
this includes the "STATE" column which is DEPRECATED and whose values are one
of 'new', 'released' or 'discontinued'. However, 'discontinued' device types
are only listed if the '--discontinued' option is also used, and this option
is also DEPRECATED.
`
.split('\n')
.join(`\n\t\t`);
export default class DevicesSupportedCmd extends Command {
public static description = stripIndent`
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
${deprecatedInfo}
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
@ -56,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 = (
@ -66,22 +52,8 @@ export default class DevicesSupportedCmd extends Command {
).trim();
public static flags: flags.Input<FlagsDef> = {
discontinued: flags.boolean({
description: isV13()
? 'No effect (DEPRECATED)'
: 'include "discontinued" device types (DEPRECATED)',
}),
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
verbose: flags.boolean({
char: 'v',
description: isV13()
? 'No effect (DEPRECATED)'
: 'add extra columns in the tabular output (DEPRECATED)',
}),
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
};
public async run() {
@ -95,60 +67,41 @@ export default class DevicesSupportedCmd extends Command {
]);
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
const discontinuedDTs = isV13()
? []
: configDTs.filter((dt) => dt.state === 'DISCONTINUED');
const discontinuedDTsBySlug = _.keyBy(discontinuedDTs, (dt) => dt.slug);
// set of slugs from models.deviceType.getAllSupported() plus slugs of
// discontinued device types as per models.config.getDeviceTypes()
const slugsOfInterest = new Set([
...Object.keys(dtsBySlug),
...Object.keys(discontinuedDTsBySlug),
]);
interface DT {
slug: string;
aliases: string[];
aliases: string[] | string;
arch: string;
state?: string; // to be removed in CLI v13
name: string;
}
let deviceTypes: DT[] = [];
for (const slug of slugsOfInterest) {
for (const slug of Object.keys(dtsBySlug)) {
const configDT: Partial<typeof configDTs[0]> =
configDTsBySlug[slug] || {};
if (configDT.state === 'DISCONTINUED' && !options.discontinued) {
continue;
}
const dt: Partial<typeof dts[0]> = dtsBySlug[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 ||
configDT.arch ||
'n/a',
// 'BETA' renamed to 'NEW'
// https://www.flowdock.com/app/rulemotion/i-cli/threads/1svvyaf8FAZeSdG4dPJc4kHOvJU
state: isV13()
? undefined
: (configDT.state || 'NEW').replace('BETA', 'NEW'),
name: dt.name || configDT.name || 'N/A',
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 =
options.verbose && !isV13()
? ['slug', 'aliases', 'arch', 'state', 'name']
: ['slug', 'aliases', 'arch', 'name'];
const fields = ['slug', 'aliases', 'arch', 'name'];
deviceTypes = _.sortBy(deviceTypes, fields);
if (options.json) {
console.log(JSON.stringify(deviceTypes, null, 4));
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

@ -21,15 +21,9 @@ import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string; // device UUID
help: void;
@ -101,11 +95,8 @@ export default class EnvAddCmd extends Command {
public static usage = 'env add <name> [value]';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: { application: { ...cf.application, exclusive: ['fleet', 'device'] } }),
fleet: { ...cf.fleet, exclusive: ['application', 'device'] },
device: { ...cf.device, exclusive: ['application', 'fleet'] },
fleet: { ...cf.fleet, exclusive: ['device'] },
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
quiet: cf.quiet,
service: cf.service,
@ -117,11 +108,7 @@ export default class EnvAddCmd extends Command {
);
const cmd = this;
if (options.application && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
if (!options.application && !options.device) {
if (!options.fleet && !options.device) {
throw new ExpectedError(
'Either the --fleet or the --device option must be specified',
);
@ -163,11 +150,12 @@ 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,
);
@ -201,8 +189,8 @@ async function setServiceVars(
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);

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2021 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,42 +21,36 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
appToFleetOutputMsg,
warnify,
} from '../utils/messages';
import { isV13 } from '../utils/version';
import { applicationIdInfo } from '../utils/messages';
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;
v13: 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
}
@ -96,8 +90,6 @@ export default class EnvsCmd extends Command {
in case the current user was removed from the fleet by the fleet's owner).
${applicationIdInfo.split('\n').join('\n\t\t')}
${appToFleetOutputMsg.split('\n').join('\n\t\t')}
`;
public static examples = [
@ -115,57 +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: 'No-op since balena CLI v12.0.0.',
hidden: true,
}),
application: {
exclusive: ['device', 'fleet', 'v13'],
...cf.application,
},
}),
fleet: { exclusive: ['device', 'application'], ...cf.fleet },
fleet: { ...cf.fleet, exclusive: ['device'] },
config: flags.boolean({
default: false,
char: 'c',
description: 'show configuration variables only',
exclusive: ['service'],
}),
device: { exclusive: ['fleet', 'application'], ...cf.device },
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
json: cf.json,
verbose: cf.verbose,
service: { exclusive: ['config'], ...cf.service },
v13: cf.v13,
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
service: { ...cf.service, exclusive: ['config'] },
};
protected useAppWord = false;
protected hasWarned = false;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
this.useAppWord = !options.fleet && !options.v13 && !isV13();
const variables: EnvironmentVariableInfo[] = [];
await Command.checkLoggedIn();
if (options.application && !options.json && process.stderr.isTTY) {
this.hasWarned = true;
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
if (!options.application && !options.device) {
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) {
@ -178,23 +148,23 @@ export default class EnvsCmd extends Command {
);
fullUUID = device.uuid;
if (app) {
appNameOrSlug = app.slug;
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
? `fleet "${options.application}"`
(options.fleet
? `fleet "${options.fleet}"`
: `device "${options.device}"`);
throw new ExpectedError(`No environment variables found for ${target}`);
}
@ -206,45 +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) => {
if (i.appName) {
// use slug in v13, app name in v12 for compatibility
i.appName = isV13()
? i.appName
: i.appName.substring(i.appName.indexOf('/') + 1);
} else {
i.appName = options.json ? null : 'N/A';
}
i.fleet ||= options.json ? null : 'N/A';
return i;
});
const jName = this.useAppWord ? 'appName' : 'fleetName';
const tName = this.useAppWord ? 'APPLICATION' : 'FLEET';
fields.push(options.json ? `appName => ${jName}` : `appName => ${tName}`);
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
if (!options.config) {
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
}
if (isV14()) {
const results = [...varArray] as any;
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 {
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
console.error(warnify(appToFleetOutputMsg));
// 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 {
// 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,
),
);
}
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
fields,
),
);
}
}
}
@ -252,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 fleet "${appName}"`,
`Service "${serviceName}" not found for fleet "${fleetSlug}"`,
);
}
}
@ -273,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> = {
@ -299,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;
@ -315,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!;
@ -324,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> = {
@ -345,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;
@ -367,7 +359,7 @@ function fillInInfoFields(
| EnvironmentVariableInfo[]
| DeviceServiceEnvironmentVariableInfo[]
| ServiceEnvironmentVariableInfo[],
appNameOrSlug?: string,
fleetSlug?: string,
deviceUUID?: string,
) {
for (const envVar of varArray) {
@ -381,7 +373,7 @@ function fillInInfoFields(
?.installs__service as SDK.Service[]
)[0]?.service_name;
}
envVar.appName = appNameOrSlug;
envVar.fleet = fleetSlug;
envVar.serviceName = envVar.serviceName || '*';
envVar.deviceUUID = deviceUUID || '*';
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 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,6 +15,135 @@
* limitations under the License.
*/
import { FleetCreateCmd } from '../app/create';
import { flags } from '@oclif/command';
import type { Application } from 'balena-sdk';
export default FleetCreateCmd;
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
export default class FleetCreateCmd extends Command {
public static description = stripIndent`
Create a fleet.
Create a new balena fleet.
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 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.
Interactive dropdowns will be shown for selection if no device type or
organization is specified and there are multiple options to choose from.
If there is a single option to choose from, it will be chosen automatically.
This interactive behavior can be disabled by explicitly specifying a device
type and organization.
`;
public static examples = [
'$ 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: 'fleet name',
required: true,
},
];
public static usage = 'fleet create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the fleet should belong to',
}),
type: flags.string({
char: 't',
description:
'fleet device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetCreateCmd,
);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
let application: Application;
try {
application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
});
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`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 fleets in organization "${organization}".`,
);
}
throw err;
}
// Output
console.log(
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
);
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk());
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 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,6 +15,89 @@
* limitations under the License.
*/
import { FleetCmd } from '../app';
import type { flags } from '@oclif/command';
import type { Release } from 'balena-sdk';
export default FleetCmd;
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 { isV14 } from '../../utils/version';
import type { DataOutputOptions } from '../../framework';
interface FlagsDef extends DataOutputOptions {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetCmd extends Command {
public static description = stripIndent`
Display information about a single fleet.
Display detailed information about a single fleet.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet MyFleet',
'$ balena fleet myorg/myfleet',
];
public static args = [ca.fleetRequired];
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, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetCmd,
);
const { getApplication } = await import('../../utils/sdk');
const application = (await getApplication(getBalenaSdk(), params.fleet, {
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
},
})) as ApplicationWithDeviceType & {
should_be_running__release: [Release?];
// For display purposes:
device_type: string;
commit?: string;
};
application.device_type = application.is_for__device_type[0].slug;
application.commit = application.should_be_running__release[0]?.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

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* 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.
@ -15,6 +15,67 @@
* limitations under the License.
*/
import { FleetPurgeCmd } from '../app/purge';
import type { flags } from '@oclif/command';
export default FleetPurgeCmd;
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetPurgeCmd extends Command {
public static description = stripIndent`
Purge data from a fleet.
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 fleet purge MyFleet',
'$ balena fleet purge myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet purge <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// balena.models.application.purge only accepts a numeric id
// so we must first fetch the app to get it's id,
const application = await getApplication(balena, params.fleet);
try {
await balena.models.application.purge(application.id);
} catch (e) {
if (e.message.toLowerCase().includes('no online device(s) found')) {
// application.purge throws an error if no devices are online
// ignore in this case.
} else {
throw e;
}
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* 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.
@ -15,6 +15,135 @@
* limitations under the License.
*/
import { FleetRenameCmd } from '../app/rename';
import type { flags } from '@oclif/command';
import type { ApplicationType } from 'balena-sdk';
export default FleetRenameCmd;
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';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
newName?: string;
}
export default class FleetRenameCmd extends Command {
public static description = stripIndent`
Rename a fleet.
Rename a fleet.
Note, if the \`newName\` parameter is omitted, it will be
prompted for interactively.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet rename OldName',
'$ balena fleet rename OldName NewName',
'$ balena fleet rename myorg/oldname NewName',
];
public static args = [
ca.fleetRequired,
{
name: 'newName',
description: 'the new name for the fleet',
},
];
public static usage = 'fleet rename <fleet> [newName]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
const { validateApplicationName } = await import('../../utils/validation');
const { ExpectedError } = await import('../../errors');
const balena = getBalenaSdk();
// 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.fleet, {
$expand: {
application_type: {
$select: ['is_legacy'],
},
},
});
// Check app exists
if (!application) {
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(
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
);
}
// Ascertain new name
const newName =
params.newName ||
(await getCliForm().ask({
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: new fleet name may only include characters [a-zA-Z0-9_-].`,
);
}
throw e;
}
// Get application again, to be sure of results
const renamedApplication = await balena.models.application.get(
application.id,
);
// Output result
console.log(`Fleet renamed`);
console.log('From:');
console.log(`\tname: ${application.app_name}`);
console.log(`\tslug: ${application.slug}`);
console.log('To:');
console.log(`\tname: ${renamedApplication.app_name}`);
console.log(`\tslug: ${renamedApplication.slug}`);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* 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.
@ -15,6 +15,56 @@
* limitations under the License.
*/
import { FleetRestartCmd } from '../app/restart';
import type { flags } from '@oclif/command';
export default FleetRestartCmd;
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetRestartCmd extends Command {
public static description = stripIndent`
Restart a fleet.
Restart all devices belonging to a fleet.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet restart MyFleet',
'$ balena fleet restart myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet restart <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
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.fleet);
await balena.models.application.restart(application.id);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* 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.
@ -15,6 +15,70 @@
* limitations under the License.
*/
import { FleetRmCmd } from '../app/rm';
import type { flags } from '@oclif/command';
export default FleetRmCmd;
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetRmCmd extends Command {
public static description = stripIndent`
Remove a fleet.
Permanently remove a fleet.
The --yes option may be used to avoid interactive confirmation.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet rm MyFleet',
'$ balena fleet rm MyFleet --yes',
'$ balena fleet rm myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet rm <fleet>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetRmCmd,
);
const { confirm } = await import('../../utils/patterns');
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// Confirm
await confirm(
options.yes ?? false,
`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.fleet);
// Remove
await balena.models.application.remove(application.id);
}
}

View File

@ -15,6 +15,94 @@
* limitations under the License.
*/
import { FleetsCmd } from './apps';
import { flags } from '@oclif/command';
export default FleetsCmd;
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

@ -21,10 +21,8 @@ import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import { parseAsLocalHostnameOrIp } from '../utils/validation';
import { isV13 } from '../utils/version';
interface FlagsDef {
application?: string;
fleet?: string;
pollInterval?: number;
help?: void;
@ -77,7 +75,6 @@ export default class JoinCmd extends Command {
public static usage = 'join [deviceIpOrHostname]';
public static flags: flags.Input<FlagsDef> = {
...(isV13() ? {} : { application: cf.application }),
fleet: cf.fleet,
pollInterval: flags.integer({
description: 'the interval in minutes to check for updates',
@ -101,7 +98,7 @@ export default class JoinCmd extends Command {
logger,
sdk,
params.deviceIpOrHostname,
options.application || options.fleet,
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

@ -56,6 +56,7 @@ 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);
@ -96,7 +97,7 @@ export default class LocalConfigureCmd extends Command {
readonly CONNECTIONS_FOLDER = '/system-connections';
getConfigurationSchema(bootPartition: number, connectionFileName?: string) {
getConfigurationSchema(bootPartition?: number, connectionFileName?: string) {
connectionFileName ??= 'resin-wifi';
return {
mapper: [
@ -112,6 +113,12 @@ export default class LocalConfigureCmd extends Command {
},
domain: [['config_json', 'hostname']],
},
{
template: {
developmentMode: '{{developmentMode}}',
},
domain: [['config_json', 'developmentMode']],
},
{
template: {
wifi: {
@ -162,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',
@ -235,9 +249,9 @@ export default class LocalConfigureCmd extends Command {
async prepareConnectionFile(target: string) {
const _ = await import('lodash');
const imagefs = await import('balena-image-fs');
const helpers = await import('../../utils/helpers');
const { getBootPartition } = await import('balena-config-json');
const bootPartition = await helpers.getBootPartition(target);
const bootPartition = await getBootPartition(target);
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);

View File

@ -65,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,

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,32 +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,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
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 {
@ -57,17 +51,15 @@ 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.
@ -81,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 fleet's default device
type, in case of a fleet 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
@ -103,7 +95,6 @@ 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 --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',
@ -126,25 +117,11 @@ export default class OsConfigureCmd extends Command {
description:
'ask advanced configuration questions (when in interactive mode)',
}),
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', '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:
@ -160,12 +137,8 @@ export default class OsConfigureCmd extends Command {
'config-wifi-ssid': flags.string({
description: 'WiFi SSID (network name) (non-interactive configuration)',
}),
device: { exclusive: ['app', 'application', 'fleet'], ...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 fleet device type',
@ -184,17 +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,
);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
await validateOptions(options);
@ -221,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' },
},
@ -242,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);
}
@ -300,7 +278,8 @@ export default class OsConfigureCmd extends Command {
}),
);
const bootPartition = await helpers.getBootPartition(params.image);
const { getBootPartition } = await import('balena-config-json');
const bootPartition = await getBootPartition(params.image);
const imagefs = await import('balena-image-fs');
@ -320,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 '--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 '--fleet' option",
);
}
if (options['device-api-key']) {
console.error(stripIndent`
-------------------------------------------------------------------------------------------
Warning: ${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t\t')}
-------------------------------------------------------------------------------------------
`);
}
await Command.checkLoggedIn();
}
@ -386,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 fleet ${options.application}`,
`Device type ${options['device-type']} is incompatible with fleet ${options.fleet}`,
);
}
}
@ -411,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,12 +98,48 @@ 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');
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

@ -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,16 +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

@ -24,15 +24,10 @@ import {
getVisuals,
stripIndent,
} from '../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../utils/messages';
import { applicationIdInfo } from '../utils/messages';
import type { DockerConnectionCliFlags } from '../utils/docker';
import { dockerConnectionCliFlags } from '../utils/docker';
import { parseAsInteger } from '../utils/validation';
import { isV13 } from '../utils/version';
import { flags } from '@oclif/command';
import * as _ from 'lodash';
@ -40,7 +35,6 @@ 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;
@ -99,7 +93,6 @@ export default class PreloadCmd extends Command {
public static usage = 'preload <image>';
public static flags: flags.Input<FlagsDef> = {
...(isV13() ? {} : { app: cf.application }),
fleet: cf.fleet,
commit: flags.string({
description: `\
@ -163,11 +156,6 @@ Can be repeated to add multiple certificates.\
PreloadCmd,
);
if (options.app && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.app ||= options.fleet;
const balena = getBalenaSdk();
const balenaPreload = await import('balena-preload');
const visuals = getVisuals();
@ -194,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(`Fleet 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'];
@ -238,15 +220,12 @@ 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 a fleet if you disable the architecture check.',
);
@ -265,7 +244,7 @@ Can be repeated to add multiple certificates.\
const preloader = new balenaPreload.Preloader(
null,
docker,
appId,
fleetSlug,
commit,
image,
splashImage,
@ -282,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;
}
});
@ -302,7 +288,7 @@ Can be repeated to add multiple certificates.\
preloader.on('error', reject);
resolve(
this.prepareAndPreload(preloader, balena, {
appId,
appId: fleetSlug,
commit,
pinDevice,
}),
@ -357,8 +343,8 @@ Can be repeated to add multiple certificates.\
} catch {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return (await balena.models.application.getAll({
$select: ['id', 'app_name', 'should_track_latest_release'],
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
@ -401,7 +387,7 @@ Can be repeated to add multiple certificates.\
},
},
},
$orderby: 'app_name asc',
$orderby: 'slug asc',
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
@ -430,7 +416,7 @@ Can be repeated to add multiple certificates.\
message: 'Select a fleet',
type: 'list',
choices: applications.map((app) => ({
name: app.app_name,
name: app.slug,
value: app,
})),
});
@ -505,7 +491,7 @@ Would you like to disable automatic updates for this fleet 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, {

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 {
@ -47,14 +46,11 @@ interface FlagsDef {
pull: boolean;
'noparent-check': boolean;
'registry-secrets'?: string;
gitignore?: boolean; // v13: delete this flag
nogitignore?: boolean; // v13: delete this flag
nolive: boolean;
detached: boolean;
service?: string[];
system: boolean;
env?: string[];
'convert-eol'?: boolean;
'noconvert-eol': boolean;
'multi-dockerignore': boolean;
'release-tag'?: string[];
@ -218,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,
@ -237,28 +223,7 @@ export default class PushCmd extends Command {
'Have each service use its own .dockerignore file. See "balena help push".',
char: 'm',
default: false,
exclusive: ['gitignore'], // v13: delete this line
}),
...(isV13()
? {}
: {
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.`,
char: 'g',
default: false,
exclusive: ['multi-dockerignore'],
}),
nogitignore: flags.boolean({
description:
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
char: 'G',
hidden: true,
default: false,
}),
}),
'release-tag': flags.string({
description: stripIndent`
Set release tags if the image build is successful (balenaCloud only). Multiple
@ -378,7 +343,6 @@ export default class PushCmd extends Command {
source: options.source,
auth: token,
baseUrl,
nogitignore: !options.gitignore, // v13: delete this line
sdk,
opts,
};
@ -422,7 +386,6 @@ export default class PushCmd extends Command {
multiDockerignore: options['multi-dockerignore'],
nocache: options.nocache,
pull: options.pull,
nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'],
nolive: options.nolive,
detached: options.detached,

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,8 +22,11 @@ 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';
interface FlagsDef {
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataOutputOptions {
help: void;
composition?: boolean;
}
@ -49,7 +52,9 @@ export default class ReleaseCmd extends Command {
default: false,
char: 'c',
description: 'Return the release composition',
exclusive: ['json', 'fields'],
}),
...(isV14() ? cf.dataOutputFlags : {}),
};
public static args = [
@ -68,29 +73,27 @@ export default class ReleaseCmd extends Command {
ReleaseCmd,
);
const balena = getBalenaSdk();
if (options.composition) {
await this.showComposition(params.commitOrId, balena);
await this.showComposition(params.commitOrId);
} else {
await this.showReleaseInfo(params.commitOrId, balena);
await this.showReleaseInfo(params.commitOrId, options);
}
}
async showComposition(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
const release = await balena.models.release.get(commitOrId, {
async showComposition(commitOrId: string | number) {
const release = await getBalenaSdk().models.release.get(commitOrId, {
$select: 'composition',
});
console.log(jsyaml.dump(release.composition));
if (isV14()) {
this.outputMessage(jsyaml.dump(release.composition));
} else {
// Old output implementation
console.log(jsyaml.dump(release.composition));
}
}
async showReleaseInfo(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
async showReleaseInfo(commitOrId: string | number, options: FlagsDef) {
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
@ -103,7 +106,7 @@ export default class ReleaseCmd extends Command {
'end_timestamp',
];
const release = await balena.models.release.get(commitOrId, {
const release = await getBalenaSdk().models.release.get(commitOrId, {
$select: fields,
$expand: {
release_tag: {
@ -116,13 +119,28 @@ export default class ReleaseCmd extends Command {
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
.join('\n');
const _ = await import('lodash');
const values = _.mapValues(
release,
(val) => val ?? 'N/a',
) as Dictionary<string>;
values['tags'] = tagStr;
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']));
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
}
}
}

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,8 +21,11 @@ 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';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
@ -43,13 +46,14 @@ export default class ReleasesCmd extends Command {
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',
description: 'fleet name or slug (preferred)',
required: true,
},
];
@ -57,7 +61,9 @@ export default class ReleasesCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ReleasesCmd,
);
const fields: Array<keyof BalenaSdk.Release> = [
'id',
@ -69,18 +75,27 @@ export default class ReleasesCmd extends Command {
];
const balena = getBalenaSdk();
const { getFleetSlug } = await import('../utils/sdk');
const releases = await balena.models.release.getAllByApplication(
params.fleet,
await getFleetSlug(balena, params.fleet),
{ $select: fields },
);
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
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');

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

@ -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>(
@ -144,6 +145,7 @@ 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,

View File

@ -20,15 +20,9 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../utils/messages';
import { isV13 } from '../utils/version';
import { applicationIdInfo } from '../utils/messages';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string;
duration?: string;
@ -77,11 +71,10 @@ export default class SupportCmd extends Command {
description: 'comma-separated list (no spaces) of device UUIDs',
char: 'd',
}),
...(isV13() ? {} : { application: cf.application }),
fleet: {
...cf.fleet,
description:
'comma-separated list (no spaces) of fleet names or org/name slugs',
'comma-separated list (no spaces) of fleet names or slugs (preferred)',
},
duration: flags.string({
description:
@ -98,18 +91,13 @@ export default class SupportCmd extends Command {
SupportCmd,
);
if (options.application && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
const balena = getBalenaSdk();
const ux = getCliUx();
const enabling = params.action === 'enable';
// Validation
if (!options.device && !options.application) {
if (!options.device && !options.fleet) {
throw new ExpectedError('At least one device or fleet must be specified');
}
@ -125,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';
@ -142,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} fleet ${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} fleet ${appName}`);
await balena.models.application.revokeSupportAccess(appName);
ux.action.start(`${disablingMessage} fleet ${slug}`);
await balena.models.application.revokeSupportAccess(slug);
}
ux.action.stop();
}

View File

@ -19,16 +19,9 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
app?: string;
application?: string;
fleet?: string;
device?: string;
release?: string;
@ -67,29 +60,17 @@ export default class TagRmCmd extends Command {
public static usage = 'tag rm <tagKey>';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device', 'release'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device', 'release'],
exclusive: ['device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'fleet', 'release'],
exclusive: ['fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'fleet', 'device'],
exclusive: ['fleet', 'device'],
},
help: cf.help,
};
@ -101,25 +82,20 @@ export default class TagRmCmd extends Command {
TagRmCmd,
);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
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,
);
}

View File

@ -19,16 +19,9 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
app?: string;
application?: string;
fleet?: string;
device?: string;
release?: string;
@ -80,29 +73,17 @@ export default class TagSetCmd extends Command {
public static usage = 'tag set <tagKey> [value]';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device', 'release'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device', 'release'],
exclusive: ['device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'fleet', 'release'],
exclusive: ['fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'fleet', 'device'],
exclusive: ['fleet', 'device'],
},
help: cf.help,
};
@ -114,15 +95,10 @@ export default class TagSetCmd extends Command {
TagSetCmd,
);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
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);
}
@ -131,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,
);

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,16 +20,13 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../utils/messages';
import { isV13 } from '../utils/version';
import { applicationIdInfo } from '../utils/messages';
import type { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
app?: string;
application?: string;
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
device?: string;
release?: string;
@ -56,30 +53,19 @@ export default class TagsCmd extends Command {
public static usage = 'tags';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device', 'release'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device', 'release'],
exclusive: ['device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'fleet', 'release'],
exclusive: ['fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'fleet', 'device'],
exclusive: ['fleet', 'device'],
},
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
@ -88,26 +74,21 @@ export default class TagsCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
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) {
@ -127,11 +108,17 @@ 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`

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,

View File

@ -104,7 +104,11 @@ export class DeprecationChecker {
const url = this.getNpmUrl(version);
let response: import('got').Response<Dictionary<any>> | undefined;
try {
response = await got(url, { responseType: 'json', retry: 0 });
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) {

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.`);

View File

@ -16,7 +16,7 @@
*/
import * as packageJSON from '../package.json';
import { getBalenaSdk } from './utils/lazy';
import { getBalenaSdk, stripIndent } from './utils/lazy';
interface CachedUsername {
token: string;
@ -129,10 +129,20 @@ async function sendEvent(balenaUrl: string, event: string, username?: string) {
data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
};
try {
await got(url, { searchParams, retry: 0 });
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
}
}

View File

@ -27,14 +27,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
// `@types/node` does not know about `options: { bigint?: boolean }`
type statT = (
fPath: string,
options: { bigint?: boolean },
) => fs.Stats | Promise<fs.Stats>;
// async stat does not work with pkg's internal `/snapshot` filesystem
const stat: statT = process.pkg ? fs.statSync : fs.promises.stat;
const stat = process.pkg ? fs.statSync : fs.promises.stat;
let fastBootStarted = false;

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

@ -53,6 +53,12 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
if (extractBooleanFlag(cmdSlice, '--debug')) {
process.env.DEBUG = '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');
}
@ -131,6 +137,13 @@ Please use "balena ${alternative}" instead.`);
'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) {

View File

@ -58,7 +58,13 @@ export function normalizeEnvVar(varName: string) {
process.env[varName] = parseBoolEnvVar(varName) ? '1' : '';
}
const bootstrapVars = ['DEBUG', 'BALENARC_NO_SENTRY'];
const bootstrapVars = [
'BALENARC_NO_SENTRY',
'BALENARC_NO_ANALYTICS',
'BALENARC_OFFLINE_MODE',
'BALENARC_UNSUPPORTED',
'DEBUG',
];
export function normalizeEnvVars(varNames: string[] = bootstrapVars) {
for (const varName of varNames) {
@ -66,6 +72,17 @@ export function normalizeEnvVars(varNames: string[] = bootstrapVars) {
}
}
/**
* Set the individual env vars implied by BALENARC_OFFLINE_MODE.
*/
export function setOfflineModeEnvVars() {
if (process.env.BALENARC_OFFLINE_MODE) {
process.env.BALENARC_UNSUPPORTED = '1';
process.env.BALENARC_NO_SENTRY = '1';
process.env.BALENARC_NO_ANALYTICS = '1';
}
}
/**
* Implements the 'pkgExec' command, used as a way to provide a Node.js
* interpreter for child_process.spawn()-like operations when the CLI is

View File

@ -107,6 +107,16 @@ export const getDeviceAndMaybeAppFromUUID = _.memoize(
(_sdk, deviceUUID) => deviceUUID,
);
/** Given a device type alias like 'nuc', return the actual slug like 'intel-nuc'. */
export const unaliasDeviceType = _.memoize(async function (
sdk: SDK.BalenaSDK,
deviceType: string,
): Promise<string> {
return (
(await sdk.models.device.getManifestBySlug(deviceType)).slug || deviceType
);
});
/**
* Download balenaOS image for the specified `deviceType`.
* `OSVersion` may be one of:
@ -130,15 +140,13 @@ export async function downloadOSImage(
console.info(`Getting device operating system for ${deviceType}`);
if (!OSVersion) {
console.warn('OS version not specified: using latest stable version');
console.warn('OS version not specified: using latest released version');
}
OSVersion = OSVersion
? await resolveOSVersion(deviceType, OSVersion)
: 'default';
const displayVersion = OSVersion === 'default' ? '' : ` ${OSVersion}`;
// Override the default zlib flush value as we've seen cases of
// incomplete files being identified as successful downloads when using Z_SYNC_FLUSH.
// Using Z_NO_FLUSH results in a Z_BUF_ERROR instead of a corrupt image file.
@ -150,10 +158,17 @@ export async function downloadOSImage(
const manager = await import('balena-image-manager');
const stream = await manager.get(deviceType, OSVersion);
const displayVersion = await new Promise((resolve, reject) => {
stream.on('error', reject);
stream.on('balena-image-manager:resolved-version', resolve);
});
const visuals = getVisuals();
const bar = new visuals.Progress(`Downloading Device OS${displayVersion}`);
const bar = new visuals.Progress(
`Downloading balenaOS version ${displayVersion}`,
);
const spinner = new visuals.Spinner(
`Downloading Device OS${displayVersion} (size unknown)`,
`Downloading balenaOS version ${displayVersion} (size unknown)`,
);
stream.on('progress', (state: any) => {
@ -183,34 +198,83 @@ export async function downloadOSImage(
const streamToPromise = await import('stream-to-promise');
await streamToPromise(stream.pipe(output));
console.info('The image was downloaded successfully');
console.info(
`balenaOS image version ${displayVersion} downloaded successfully`,
);
return outputPath;
}
async function resolveOSVersion(deviceType: string, version: string) {
if (version !== 'menu') {
async function resolveOSVersion(
deviceType: string,
version: string,
): Promise<string> {
if (['menu', 'menu-esr'].includes(version)) {
return await selectOSVersionFromMenu(deviceType, version === 'menu-esr');
}
// Note that `version` may also be 'latest', 'recommended', 'default'
if (/^v?\d+\.\d+\.\d+/.test(version)) {
if (version[0] === 'v') {
version = version.slice(1);
}
return version;
}
return version;
}
const vs = (
(await getBalenaSdk().models.hostapp.getAllOsVersions([deviceType]))[
deviceType
] ?? []
).filter((v) => v.osType === 'default');
async function selectOSVersionFromMenu(
deviceType: string,
esr: boolean,
): Promise<string> {
const vs = await getOsVersions(deviceType, esr);
const choices = vs.map((v) => ({
value: v.rawVersion,
name: `v${v.rawVersion}` + (v.isRecommended ? ' (recommended)' : ''),
value: v.raw_version,
name: formatOsVersion(v),
}));
return getCliForm().ask({
message: 'Select the OS version:',
type: 'list',
choices,
default: (vs.find((v) => v.isRecommended) ?? vs[0])?.rawVersion,
default: vs[0]?.raw_version,
});
}
/**
* Return the output of sdk.models.os.getAvailableOsVersions(), resolving
* device type aliases and filtering with regard to ESR versions.
*/
export async function getOsVersions(
deviceType: string,
esr: boolean,
): Promise<SDK.OsVersion[]> {
const sdk = getBalenaSdk();
let slug = deviceType;
let versions: SDK.OsVersion[] = await sdk.models.os.getAvailableOsVersions(
slug,
);
// if slug is an alias, fetch the real slug
if (!versions.length) {
// unaliasDeviceType() produces a nice error msg if slug is invalid
slug = await unaliasDeviceType(sdk, slug);
if (slug !== deviceType) {
versions = await sdk.models.os.getAvailableOsVersions(slug);
}
}
versions = versions.filter(
(v: SDK.OsVersion) => v.osType === (esr ? 'esr' : 'default'),
);
if (!versions.length) {
const vType = esr ? 'ESR versions' : 'versions';
throw new ExpectedError(
`Error: No balenaOS ${vType} found for device type '${deviceType}'.`,
);
}
return versions;
}
export function formatOsVersion(osVersion: SDK.OsVersion): string {
return osVersion.line
? `v${osVersion.raw_version} (${osVersion.line})`
: `v${osVersion.raw_version}`;
}

View File

@ -16,37 +16,16 @@
*/
import { flags } from '@oclif/command';
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
import { stripIndent } from './lazy';
import { lowercaseIfSlug } from './normalization';
import { isV13 } from './version';
export const v13: IBooleanFlag<boolean> = flags.boolean({
description: stripIndent`\
enable selected balena CLI v13 pre-release features, like the renaming
from "application" to "fleet" in command output`,
default: false,
});
import { isV14 } from './version';
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
export const application = flags.string({
char: 'a',
description: 'DEPRECATED alias for -f, --fleet',
parse: lowercaseIfSlug,
});
// TODO: Consider remove second alias 'app' when we can, to simplify.
export const app = flags.string({
description: 'DEPRECATED alias for -f, --fleet',
parse: lowercaseIfSlug,
});
export const fleet = flags.string({
char: 'f',
description: isV13()
? 'fleet name, slug (preferred), or numeric ID (deprecated)'
: // avoid the '(deprecated)' remark in v12 while cf.application and
// cf.app are also described as deprecated, to avoid the impression
// that cf.fleet is deprecated as well.
'fleet name, slug (preferred), or numeric ID',
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
parse: lowercaseIfSlug,
});
@ -91,6 +70,11 @@ export const force: IBooleanFlag<boolean> = flags.boolean({
default: false,
});
export const dev: IBooleanFlag<boolean> = flags.boolean({
description: 'Configure balenaOS to operate in development mode',
default: false,
});
export const drive = flags.string({
char: 'd',
description: stripIndent`
@ -113,8 +97,55 @@ export const deviceType = flags.string({
required: true,
});
export const deviceTypeIgnored = {
...(isV14()
? {}
: {
type: flags.string({
description: 'ignored - no longer required',
char: 't',
required: false,
hidden: true,
}),
}),
};
export const json: IBooleanFlag<boolean> = flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
default: false,
});
export const dataOutputFlags: flags.Input<DataOutputOptions> = {
fields: flags.string({
description: 'only show provided fields (comma-separated)',
}),
json: flags.boolean({
char: 'j',
exclusive: ['no-truncate'],
description: 'output in json format',
default: false,
}),
};
export const dataSetOutputFlags: flags.Input<DataOutputOptions> &
flags.Input<DataSetOutputOptions> = {
...dataOutputFlags,
filter: flags.string({
description:
'filter results by substring matching of a given field, eg: --filter field=foo',
}),
'no-header': flags.boolean({
exclusive: ['json'],
description: 'hide table header from output',
default: false,
}),
'no-truncate': flags.boolean({
exclusive: ['json'],
description: 'do not truncate output to fit screen',
default: false,
}),
sort: flags.string({
description: `field to sort by (prepend '-' for descending order)`,
}),
};

View File

@ -15,6 +15,7 @@
* limitations under the License.
*/
import type { ImageModel, ReleaseModel } from 'balena-release/build/models';
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
import type { Pack } from 'tar-stream';
@ -51,9 +52,8 @@ export interface ComposeOpts {
dockerfilePath?: string;
inlineLogs?: boolean;
multiDockerignore: boolean;
nogitignore: boolean; // v13: delete this line
noParentCheck: boolean;
projectName: string;
projectName?: string;
projectPath: string;
isLocal?: boolean;
}
@ -63,12 +63,9 @@ export interface ComposeCliFlags {
dockerfile?: string;
logs: boolean;
nologs: boolean;
gitignore?: boolean; // v13: delete this line
nogitignore?: boolean; // v13: delete this line
'multi-dockerignore': boolean;
'noparent-check': boolean;
'registry-secrets'?: RegistrySecrets;
'convert-eol': boolean;
'noconvert-eol': boolean;
projectName?: string;
}
@ -83,7 +80,7 @@ export interface ComposeProject {
export interface Release {
client: ReturnType<typeof import('balena-release').createClient>;
release: Pick<
import('balena-release/build/models').ReleaseModel,
ReleaseModel,
| 'id'
| 'status'
| 'commit'
@ -95,13 +92,14 @@ export interface Release {
| 'start_timestamp'
| 'end_timestamp'
>;
serviceImages: Partial<import('balena-release/build/models').ImageModel>;
serviceImages: Dictionary<
Omit<ImageModel, 'created_at' | 'is_a_build_of__service' | '__metadata'>
>;
}
interface TarDirectoryOptions {
composition?: Composition;
convertEol?: boolean;
multiDockerignore?: boolean;
nogitignore: boolean; // v13: delete this line
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
}

View File

@ -15,22 +15,32 @@
* limitations under the License.
*/
import type { Renderer } from './compose_ts';
import type * as SDK from 'balena-sdk';
import type Dockerode = require('dockerode');
import * as path from 'path';
import { ExpectedError } from '../errors';
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
import type {
BuiltImage,
ComposeOpts,
ComposeProject,
Release,
TaggedImage,
} from './compose-types';
import { getChalk } from './lazy';
import { isV13 } from './version';
import Logger = require('./logger');
import { ProgressCallback } from 'docker-progress';
/**
* @returns Promise<{import('./compose-types').ComposeOpts}>
*/
export function generateOpts(options) {
const { promises: fs } = require('fs');
if (!isV13() && options.gitignore && options['multi-dockerignore']) {
throw new ExpectedError(
'The --gitignore and --multi-dockerignore options cannot be used together',
);
}
export function generateOpts(options: {
source?: string;
projectName?: string;
nologs: boolean;
'noconvert-eol': boolean;
dockerfile?: string;
'multi-dockerignore': boolean;
'noparent-check': boolean;
}): Promise<ComposeOpts> {
const { promises: fs } = require('fs') as typeof import('fs');
return fs.realpath(options.source || '.').then((projectPath) => ({
projectName: options.projectName,
projectPath,
@ -38,29 +48,23 @@ export function generateOpts(options) {
convertEol: !options['noconvert-eol'],
dockerfilePath: options.dockerfile,
multiDockerignore: !!options['multi-dockerignore'],
nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'],
}));
}
// Parse the given composition and return a structure with info. Input is:
// - composePath: the *absolute* path to the directory containing the compose file
// - composeStr: the contents of the compose file, as a string
/**
* @param {string} composePath
* @param {string} composeStr
* @param {string | undefined} projectName The --projectName flag (build, deploy)
* @param {string | undefined} imageTag The --tag flag (build, deploy)
* @returns {import('./compose-types').ComposeProject}
/** Parse the given composition and return a structure with info. Input is:
* - composePath: the *absolute* path to the directory containing the compose file
* - composeStr: the contents of the compose file, as a string
*/
export function createProject(
composePath,
composeStr,
composePath: string,
composeStr: string,
projectName = '',
imageTag = '',
) {
const yml = require('js-yaml');
const compose = require('resin-compose-parse');
): ComposeProject {
const yml = require('js-yaml') as typeof import('js-yaml');
const compose =
require('resin-compose-parse') as typeof import('resin-compose-parse');
// both methods below may throw.
const rawComposition = yml.load(composeStr);
@ -76,7 +80,8 @@ export function createProject(
descr.image.context != null &&
descr.image.tag == null
) {
const { makeImageName } = require('./compose_ts');
const { makeImageName } =
require('./compose_ts') as typeof import('./compose_ts');
descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag);
}
return descr;
@ -89,116 +94,20 @@ export function createProject(
};
}
/**
* This is the CLI v10 / v11 "original" tarDirectory function. It is still
* around for the benefit of the `--gitignore` option, but is expected to be
* deleted in CLI v13.
* @param {string} dir Source directory
* @param {import('./compose-types').TarDirectoryOptions} param
* @returns {Promise<import('stream').Readable>}
*
* v13: delete this function
*/
export async function originalTarDirectory(dir, param) {
let {
preFinalizeCallback = null,
convertEol = false,
nogitignore = false,
} = param;
if (convertEol == null) {
convertEol = false;
}
const Bluebird = require('bluebird');
const tar = require('tar-stream');
const klaw = require('klaw');
const { promises: fs } = require('fs');
const streamToPromise = require('stream-to-promise');
const { printGitignoreWarn } = require('./compose_ts');
const { FileIgnorer, IgnoreFileType } = require('./ignore');
const { toPosixPath } = require('resin-multibuild').PathUtils;
let readFile;
if (process.platform === 'win32') {
const { readFileWithEolConversion } = require('./eol-conversion');
readFile = (file) => readFileWithEolConversion(file, convertEol);
} else {
({ readFile } = fs);
}
const getFiles = () =>
Bluebird.resolve(streamToPromise(klaw(dir)))
// @ts-ignore
.filter((item) => !item.stats.isDirectory())
// @ts-ignore
.map((item) => item.path);
const ignore = new FileIgnorer(dir);
const pack = tar.pack();
const ignoreFiles = {};
return getFiles()
.each(function (file) {
const type = ignore.getIgnoreFileType(path.relative(dir, file));
if (type != null) {
ignoreFiles[type] = ignoreFiles[type] || [];
ignoreFiles[type].push(path.resolve(dir, file));
return ignore.addIgnoreFile(file, type);
}
})
.tap(() => {
if (!nogitignore) {
printGitignoreWarn(
(ignoreFiles[IgnoreFileType.DockerIgnore] || [])[0] || '',
ignoreFiles[IgnoreFileType.GitIgnore] || [],
);
}
})
.filter(ignore.filter)
.map(function (file) {
const relPath = path.relative(path.resolve(dir), file);
return Promise.all([relPath, fs.stat(file), readFile(file)]).then(
([filename, stats, data]) =>
pack.entry(
{
name: toPosixPath(filename),
mtime: stats.mtime,
size: stats.size,
mode: stats.mode,
},
data,
),
);
})
.then(() => preFinalizeCallback?.(pack))
.then(function () {
pack.finalize();
return pack;
});
}
/**
* @param {string} apiEndpoint
* @param {string} auth
* @param {number} userId
* @param {number} appId
* @param {import('resin-compose-parse').Composition} composition
* @param {boolean} draft
* @param {string|undefined} semver
* @param {string|undefined} contract
* @returns {Promise<import('./compose-types').Release>}
*/
export const createRelease = async function (
apiEndpoint,
auth,
userId,
appId,
composition,
draft,
semver,
contract,
) {
const _ = require('lodash');
const crypto = require('crypto');
const releaseMod = require('balena-release');
apiEndpoint: string,
auth: string,
userId: number,
appId: number,
composition: Composition,
draft: boolean,
semver?: string,
contract?: string,
): Promise<Release> {
const _ = require('lodash') as typeof import('lodash');
const crypto = require('crypto') as typeof import('crypto');
const releaseMod =
require('balena-release') as typeof import('balena-release');
const client = releaseMod.createClient({ apiEndpoint, auth });
@ -228,24 +137,26 @@ export const createRelease = async function (
'start_timestamp',
'end_timestamp',
]),
serviceImages: _.mapValues(serviceImages, (serviceImage) =>
_.omit(serviceImage, [
'created_at',
'is_a_build_of__service',
'__metadata',
]),
serviceImages: _.mapValues(
serviceImages,
(serviceImage) =>
_.omit(serviceImage, [
'created_at',
'is_a_build_of__service',
'__metadata',
]) as Omit<
typeof serviceImage,
'created_at' | 'is_a_build_of__service' | '__metadata'
>,
),
};
};
/**
*
* @param {import('dockerode')} docker
* @param {Array<import('./compose-types').BuiltImage>} images
* @param {Partial<import('balena-release/build/models').ImageModel>} serviceImages
* @returns {Promise<Array<import('./compose-types').TaggedImage>>}
*/
export const tagServiceImages = (docker, images, serviceImages) =>
export const tagServiceImages = (
docker: Dockerode,
images: BuiltImage[],
serviceImages: Release['serviceImages'],
): Promise<TaggedImage[]> =>
Promise.all(
images.map(function (d) {
const serviceImage = serviceImages[d.serviceName];
@ -272,25 +183,24 @@ export const tagServiceImages = (docker, images, serviceImages) =>
}),
);
/**
* @param {*} sdk
* @param {import('./logger')} logger
* @param {number} appID
* @returns {Promise<string[]>}
*/
export const getPreviousRepos = (sdk, logger, appID) =>
export const getPreviousRepos = (
sdk: SDK.BalenaSDK,
logger: Logger,
appID: number,
): Promise<string[]> =>
sdk.pine
.get({
.get<SDK.Release>({
resource: 'release',
options: {
$select: 'id',
$filter: {
belongs_to__application: appID,
status: 'success',
},
$select: ['id'],
$expand: {
contains__image: {
$expand: 'image',
$select: 'image',
$expand: { image: { $select: 'is_stored_at__image_location' } },
},
},
$orderby: 'id desc',
@ -300,8 +210,11 @@ export const getPreviousRepos = (sdk, logger, appID) =>
.then(function (release) {
// grab all images from the latest release, return all image locations in the registry
if (release.length > 0) {
const images = release[0].contains__image;
const { getRegistryAndName } = require('resin-multibuild');
const images = release[0].contains__image as Array<{
image: [SDK.Image];
}>;
const { getRegistryAndName } =
require('resin-multibuild') as typeof import('resin-multibuild');
return Promise.all(
images.map(function (d) {
const imageName = d.image[0].is_stored_at__image_location || '';
@ -321,21 +234,13 @@ export const getPreviousRepos = (sdk, logger, appID) =>
return [];
});
/**
* @param {*} sdk
* @param {string} tokenAuthEndpoint
* @param {string} registry
* @param {string[]} images
* @param {string[]} previousRepos
* @returns {Promise<string>}
*/
export const authorizePush = function (
sdk,
tokenAuthEndpoint,
registry,
images,
previousRepos,
) {
sdk: SDK.BalenaSDK,
tokenAuthEndpoint: string,
registry: string,
images: string[],
previousRepos: string[],
): Promise<string> {
if (!Array.isArray(images)) {
images = [images];
}
@ -354,89 +259,22 @@ export const authorizePush = function (
.catch(() => '');
};
/**
* @param {import('dockerode')} docker
* @param {string} token
* @param {Array<import('./compose-types').TaggedImage>} images
* @param {(serviceImage: import('balena-release/build/models').ImageModel, props: object) => void} afterEach
*/
export const pushAndUpdateServiceImages = function (
docker,
token,
images,
afterEach,
) {
const { DockerProgress } = require('docker-progress');
const { retry } = require('./helpers');
const tty = require('./tty')(process.stdout);
const Bluebird = require('bluebird');
const opts = { authconfig: { registrytoken: token } };
const progress = new DockerProgress({ docker });
const renderer = pushProgressRenderer(
tty,
getChalk().blue('[Push]') + ' ',
);
const reporters = progress.aggregateProgress(images.length, renderer);
return Bluebird.using(tty.cursorHidden(), () =>
Promise.all(
images.map(({ serviceImage, localImage, props, logs }, index) =>
Promise.all([
localImage.inspect().then((img) => img.Size),
retry({
// @ts-ignore
func: () => progress.push(localImage.name, reporters[index], opts),
maxAttempts: 3, // try calling func 3 times (max)
// @ts-ignore
label: localImage.name, // label for retry log messages
initialDelayMs: 2000, // wait 2 seconds before the 1st retry
backoffScaler: 1.4, // wait multiplier for each retry
}).finally(renderer.end),
])
.then(
/** @type {([number, string]) => void} */
function ([size, digest]) {
serviceImage.image_size = size;
serviceImage.content_hash = digest;
serviceImage.build_log = logs;
serviceImage.dockerfile = props.dockerfile;
serviceImage.project_type = props.projectType;
if (props.startTime) {
serviceImage.start_timestamp = props.startTime;
}
if (props.endTime) {
serviceImage.end_timestamp = props.endTime;
}
serviceImage.push_timestamp = new Date();
serviceImage.status = 'success';
},
)
.catch(function (e) {
serviceImage.error_message = '' + e;
serviceImage.status = 'failed';
throw e;
})
.finally(() => afterEach?.(serviceImage, props)),
),
),
);
};
// utilities
const renderProgressBar = function (percentage, stepCount) {
const _ = require('lodash');
const renderProgressBar = function (percentage: number, stepCount: number) {
const _ = require('lodash') as typeof import('lodash');
percentage = _.clamp(percentage, 0, 100);
const barCount = Math.floor((stepCount * percentage) / 100);
const spaceCount = stepCount - barCount;
const bar = `[${_.repeat('=', barCount)}>${_.repeat(' ', spaceCount)}]`;
return `${bar} ${_.padStart(percentage, 3)}%`;
return `${bar} ${_.padStart(`${percentage}`, 3)}%`;
};
var pushProgressRenderer = function (tty, prefix) {
const fn = function (e) {
export const pushProgressRenderer = function (
tty: ReturnType<typeof import('./tty')>,
prefix: string,
): ProgressCallback & { end: () => void } {
const fn: ProgressCallback & { end: () => void } = function (e) {
const { error, percentage } = e;
if (error != null) {
throw new Error(error);
@ -450,14 +288,39 @@ var pushProgressRenderer = function (tty, prefix) {
return fn;
};
export class BuildProgressUI {
constructor(tty, descriptors) {
export class BuildProgressUI implements Renderer {
public streams;
private _prefix;
private _prefixWidth;
private _tty;
private _services;
private _startTime: undefined | number;
private _ended;
private _serviceToDataMap: Dictionary<{
status?: string;
progress?: number;
error?: Error;
}> = {};
private _cancelled;
private _spinner;
private _runloop:
| undefined
| ReturnType<typeof import('./compose_ts').createRunLoop>;
// these are to handle window wrapping
private _maxLineWidth: undefined | number;
private _lineWidths: number[] = [];
constructor(
tty: ReturnType<typeof import('./tty')>,
descriptors: ImageDescriptor[],
) {
this._handleEvent = this._handleEvent.bind(this);
this.start = this.start.bind(this);
this.end = this.end.bind(this);
this._display = this._display.bind(this);
const _ = require('lodash');
const through = require('through2');
const _ = require('lodash') as typeof import('lodash');
const through = require('through2') as typeof import('through2');
const eventHandler = this._handleEvent;
const services = _.map(descriptors, 'serviceName');
@ -475,7 +338,6 @@ export class BuildProgressUI {
.value();
this._tty = tty;
this._serviceToDataMap = {};
this._services = services;
// Logger magically prefixes the log line with [Build] etc., but it doesn't
@ -485,22 +347,22 @@ export class BuildProgressUI {
const offset = 10; // account for escape sequences inserted for colouring
this._prefixWidth =
offset + prefix.length + _.max(_.map(services, 'length'));
offset + prefix.length + _.max(_.map(services, (s) => s.length))!;
this._prefix = prefix;
// these are to handle window wrapping
this._maxLineWidth = null;
this._lineWidths = [];
this._startTime = null;
this._ended = false;
this._cancelled = false;
this._spinner = require('./compose_ts').createSpinner();
this._spinner = (
require('./compose_ts') as typeof import('./compose_ts')
).createSpinner();
this.streams = streams;
}
_handleEvent(service, event) {
_handleEvent(
service: string,
event: { status?: string; progress?: number; error?: Error },
) {
this._serviceToDataMap[service] = event;
}
@ -509,17 +371,19 @@ export class BuildProgressUI {
this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' });
});
this._runloop = require('./compose_ts').createRunLoop(this._display);
this._runloop = (
require('./compose_ts') as typeof import('./compose_ts')
).createRunLoop(this._display);
this._startTime = Date.now();
}
end(summary = null) {
end(summary?: Dictionary<string>) {
if (this._ended) {
return;
}
this._ended = true;
this._runloop?.end();
this._runloop = null;
this._runloop = undefined;
this._clear();
this._renderStatus(true);
@ -540,7 +404,7 @@ export class BuildProgressUI {
}
_getServiceSummary() {
const _ = require('lodash');
const _ = require('lodash') as typeof import('lodash');
const services = this._services;
const serviceToDataMap = this._serviceToDataMap;
@ -567,11 +431,11 @@ export class BuildProgressUI {
.value();
}
_renderStatus(end) {
end ??= false;
const moment = require('moment');
require('moment-duration-format')(moment);
_renderStatus(end = false) {
const moment = require('moment') as typeof import('moment');
(
require('moment-duration-format') as typeof import('moment-duration-format')
)(moment);
this._tty.clearLine();
this._tty.write(this._prefix);
@ -596,11 +460,11 @@ export class BuildProgressUI {
}
}
_renderSummary(serviceToStrMap) {
const _ = require('lodash');
_renderSummary(serviceToStrMap: Dictionary<string>) {
const _ = require('lodash') as typeof import('lodash');
const chalk = getChalk();
const truncate = require('cli-truncate');
const strlen = require('string-width');
const truncate = require('cli-truncate') as typeof import('cli-truncate');
const strlen = require('string-width') as typeof import('string-width');
this._services.forEach((service, index) => {
let str = _.padEnd(this._prefix + chalk.bold(service), this._prefixWidth);
@ -616,13 +480,23 @@ export class BuildProgressUI {
}
}
export class BuildProgressInline {
constructor(outStream, descriptors) {
export class BuildProgressInline implements Renderer {
public streams;
private _prefixWidth;
private _outStream;
private _services;
private _startTime: number | undefined;
private _ended;
constructor(
outStream: NodeJS.ReadWriteStream,
descriptors: Array<{ serviceName: string }>,
) {
this.start = this.start.bind(this);
this.end = this.end.bind(this);
this._renderEvent = this._renderEvent.bind(this);
const _ = require('lodash');
const through = require('through2');
const _ = require('lodash') as typeof import('lodash');
const through = require('through2') as typeof import('through2');
const services = _.map(descriptors, 'serviceName');
const eventHandler = this._renderEvent;
@ -639,10 +513,9 @@ export class BuildProgressInline {
.value();
const offset = 10; // account for escape sequences inserted for colouring
this._prefixWidth = offset + _.max(_.map(services, 'length'));
this._prefixWidth = offset + _.max(_.map(services, (s) => s.length))!;
this._outStream = outStream;
this._services = services;
this._startTime = null;
this._ended = false;
this.streams = streams;
@ -656,9 +529,11 @@ export class BuildProgressInline {
this._startTime = Date.now();
}
end(summary = null) {
const moment = require('moment');
require('moment-duration-format')(moment);
end(summary?: Dictionary<string>) {
const moment = require('moment') as typeof import('moment');
(
require('moment-duration-format') as typeof import('moment-duration-format')
)(moment);
if (this._ended) {
return;
@ -686,8 +561,8 @@ export class BuildProgressInline {
this._outStream.write(`Built ${serviceStr} in ${durationStr}\n`);
}
_renderEvent(service, event) {
const _ = require('lodash');
_renderEvent(service: string, event: { status?: string; error?: Error }) {
const _ = require('lodash') as typeof import('lodash');
const str = (function () {
const { status, error } = event;

View File

@ -43,7 +43,6 @@ import {
import type { DeviceInfo } from './device/api';
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
import Logger = require('./logger');
import { isV13 } from './version';
import { exists } from './which';
const allowedContractTypes = ['sw.application', 'sw.block'];
@ -105,8 +104,6 @@ export async function applyReleaseTagKeysAndValues(
const LOG_LENGTH_MAX = 512 * 1024; // 512KB
const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml'];
const hr =
'----------------------------------------------------------------------';
/**
* high-level function resolving a project and creating a composition out
@ -238,7 +235,7 @@ interface BuildTaskPlus extends MultiBuild.BuildTask {
logBuffer?: string[];
}
interface Renderer {
export interface Renderer {
start: () => void;
end: (buildSummaryByService?: Dictionary<string>) => void;
streams: Dictionary<NodeJS.ReadWriteStream>;
@ -257,7 +254,6 @@ export interface BuildProjectOpts {
inlineLogs?: boolean;
convertEol: boolean;
dockerfilePath?: string;
nogitignore: boolean; // v13: delete this line
multiDockerignore: boolean;
}
@ -748,43 +744,19 @@ export function isBuildConfig(
* Create a tar stream out of the local filesystem at the given directory,
* while optionally applying file filters such as '.dockerignore' and
* optionally converting text file line endings (CRLF to LF).
* @param dir Source directory
* @param param Options
* @returns Readable stream
* @param dir Project directory (the '--source' command line option)
* @param param TarDirectoryOptions
* @returns Readable stream (to be sent to the Docker Engine)
*/
export async function tarDirectory(
dir: string,
param: TarDirectoryOptions,
): Promise<import('stream').Readable> {
const { nogitignore = false } = param; // v13: delete this line
if (isV13() || nogitignore) {
return newTarDirectory(dir, param);
} else {
return (await import('./compose')).originalTarDirectory(dir, param);
}
}
/**
* Create a tar stream out of the local filesystem at the given directory,
* while optionally applying file filters such as '.dockerignore' and
* optionally converting text file line endings (CRLF to LF).
* @param dir Source directory
* @param param Options
* @returns Readable stream
*/
async function newTarDirectory(
dir: string,
{
composition,
convertEol = false,
multiDockerignore = false,
nogitignore = false, // v13: delete this line
preFinalizeCallback,
}: TarDirectoryOptions,
): Promise<import('stream').Readable> {
if (!isV13()) {
require('assert').strict.equal(nogitignore, true);
}
const { filterFilesWithDockerignore } = await import('./ignore');
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
@ -905,48 +877,6 @@ function printDockerignoreWarn(
}
}
/**
* Print a deprecation warning if any '.gitignore' or '.dockerignore' file is
* found and the --gitignore (-g) option has been provided (v11 compatibility).
* @param dockerignoreFile Absolute path to a .dockerignore file
* @param gitignoreFiles Array of absolute paths to .gitginore files
*
* v13: delete this function
*/
export function printGitignoreWarn(
dockerignoreFile: string,
gitignoreFiles: string[],
) {
if (isV13()) {
return;
}
const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter((e) => e);
if (ignoreFiles.length === 0) {
return;
}
const msg = [' ', hr, 'Using file ignore patterns from:'];
msg.push(...ignoreFiles.map((e) => `* ${e}`));
if (gitignoreFiles.length) {
msg.push(stripIndent`
.gitignore files are being considered because the --gitignore option was used.
This option is deprecated and will be removed in the next major version release.
For more information, see 'balena help ${Logger.command}'.
`);
msg.push(hr);
Logger.getLogger().logWarn(msg.join('\n'));
} else if (dockerignoreFile && process.platform === 'win32') {
msg.push(stripIndent`
The --gitignore option was used, but no .gitignore files were found.
The --gitignore option is deprecated and will be removed in the next major
version release. It prevents the use of a better dockerignore parser and
filter library that fixes several issues on Windows and improves compatibility
with 'docker build'. For more information, see 'balena help ${Logger.command}'.
`);
msg.push(hr);
Logger.getLogger().logWarn(msg.join('\n'));
}
}
/**
* Check whether the "build secrets" feature is being used and, if so,
* verify that the target docker daemon is balenaEngine. If the
@ -1085,53 +1015,52 @@ async function performResolution(
releaseHash: string,
preprocessHook?: (dockerfile: string) => string,
): Promise<MultiBuild.BuildTask[]> {
const { cloneTarStream } = await import('tar-utils');
const multiBuild = await import('resin-multibuild');
return await new Promise<MultiBuild.BuildTask[]>((resolve, reject) => {
const buildTasks = multiBuild.performResolution(
tasks,
deviceInfo.arch,
deviceInfo.deviceType,
{ error: [reject] },
{
BALENA_RELEASE_HASH: releaseHash,
BALENA_APP_NAME: appName,
},
preprocessHook,
);
(async () => {
try {
// Do one task at a time in order to reduce peak memory usage. Resolves to buildTasks.
for (const buildTask of buildTasks) {
// buildStream is falsy for "external" tasks (image pull)
if (!buildTask.buildStream) {
continue;
}
let error: Error | undefined;
try {
// Consume each task.buildStream in order to trigger the
// resolution events that define fields like:
// task.dockerfile, task.dockerfilePath,
// task.projectType, task.resolved
// This mimics what is currently done in `resin-builder`.
buildTask.buildStream = await cloneTarStream(buildTask.buildStream);
} catch (e) {
error = e;
}
if (error || (!buildTask.external && !buildTask.resolved)) {
const cause = error ? `${error}\n` : '';
throw new ExpectedError(
`${cause}Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
);
}
}
resolve(buildTasks);
} catch (e) {
reject(e);
}
})();
const resolveListeners: MultiBuild.ResolveListeners = {};
const resolvePromise = new Promise<never>((_resolve, reject) => {
resolveListeners.error = [reject];
});
const buildTasks = multiBuild.performResolution(
tasks,
deviceInfo.arch,
deviceInfo.deviceType,
resolveListeners,
{
BALENA_RELEASE_HASH: releaseHash,
BALENA_APP_NAME: appName,
},
preprocessHook,
);
await Promise.race([resolvePromise, resolveTasks(buildTasks)]);
return buildTasks;
}
async function resolveTasks(buildTasks: MultiBuild.BuildTask[]) {
const { cloneTarStream } = await import('tar-utils');
// Do one task at a time in order to reduce peak memory usage. Resolves to buildTasks.
for (const buildTask of buildTasks) {
// buildStream is falsy for "external" tasks (image pull)
if (!buildTask.buildStream) {
continue;
}
let error: Error | undefined;
try {
// Consume each task.buildStream in order to trigger the
// resolution events that define fields like:
// task.dockerfile, task.dockerfilePath,
// task.projectType, task.resolved
// This mimics what is currently done in `resin-builder`.
buildTask.buildStream = await cloneTarStream(buildTask.buildStream);
} catch (e) {
error = e;
}
if (error || (!buildTask.external && !buildTask.resolved)) {
const cause = error ? `${error}\n` : '';
throw new ExpectedError(
`${cause}Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
);
}
}
}
/**
@ -1306,15 +1235,101 @@ async function getTokenForPreviousRepos(
return token;
}
async function pushAndUpdateServiceImages(
docker: Dockerode,
token: string,
images: TaggedImage[],
afterEach: (
serviceImage: import('balena-release/build/models').ImageModel,
props: object,
) => void,
) {
const { DockerProgress } = await import('docker-progress');
const { retry } = await import('./helpers');
const { pushProgressRenderer } = await import('./compose');
const tty = (await import('./tty'))(process.stdout);
const opts = { authconfig: { registrytoken: token } };
const progress = new DockerProgress({ docker });
const renderer = pushProgressRenderer(
tty,
getChalk().blue('[Push]') + ' ',
);
const reporters = progress.aggregateProgress(images.length, renderer);
const pushImage = async (
localImage: Dockerode.Image,
index: number,
): Promise<string> => {
try {
// TODO 'localImage as any': find out exactly why tsc warns about
// 'name' that exists as a matter of fact, with a value similar to:
// "name": "registry2.balena-cloud.com/v2/aa27790dff571ec7d2b4fbcf3d4648d5:latest"
const imgName: string = (localImage as any).name || '';
const imageDigest: string = await retry({
func: () => progress.push(imgName, reporters[index], opts),
maxAttempts: 3, // try calling func 3 times (max)
label: imgName, // label for retry log messages
initialDelayMs: 2000, // wait 2 seconds before the 1st retry
backoffScaler: 1.4, // wait multiplier for each retry
});
if (!imageDigest) {
throw new ExpectedError(stripIndent`\
Unable to extract image digest (content hash) from image upload progress stream for image:
${imgName}`);
}
return imageDigest;
} finally {
renderer.end();
}
};
const inspectAndPushImage = async (
{ serviceImage, localImage, props, logs }: TaggedImage,
index: number,
) => {
try {
const [imgInfo, imgDigest] = await Promise.all([
localImage.inspect(),
pushImage(localImage, index),
]);
serviceImage.image_size = imgInfo.Size;
serviceImage.content_hash = imgDigest;
serviceImage.build_log = logs;
serviceImage.dockerfile = props.dockerfile;
serviceImage.project_type = props.projectType;
if (props.startTime) {
serviceImage.start_timestamp = props.startTime;
}
if (props.endTime) {
serviceImage.end_timestamp = props.endTime;
}
serviceImage.push_timestamp = new Date();
serviceImage.status = 'success';
} catch (error) {
serviceImage.error_message = '' + error;
serviceImage.status = 'failed';
throw error;
} finally {
await afterEach(serviceImage, props);
}
};
tty.hideCursor();
try {
await Promise.all(images.map(inspectAndPushImage));
} finally {
tty.showCursor();
}
}
async function pushServiceImages(
docker: import('dockerode'),
docker: Dockerode,
logger: Logger,
pineClient: ReturnType<typeof import('balena-release').createClient>,
taggedImages: TaggedImage[],
token: string,
skipLogUpload: boolean,
): Promise<void> {
const { pushAndUpdateServiceImages } = await import('./compose');
const releaseMod = await import('balena-release');
logger.logInfo('Pushing images to registry...');
await pushAndUpdateServiceImages(
@ -1337,7 +1352,7 @@ async function pushServiceImages(
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
export async function deployProject(
docker: import('dockerode'),
docker: Dockerode,
logger: Logger,
composition: Composition,
images: BuiltImage[],
@ -1644,21 +1659,6 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
description:
'Hide the image build log output (produce less verbose output)',
}),
...(isV13()
? {}
: {
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted.`,
char: 'g',
}),
nogitignore: flags.boolean({
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
char: 'G',
}),
}),
'multi-dockerignore': flags.boolean({
description:
'Have each service use its own .dockerignore file. See "balena help build".',
@ -1673,10 +1673,6 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
'Path to a YAML or JSON file with passwords for a private Docker registry',
char: 'R',
}),
'convert-eol': flags.boolean({
description: 'No-op and deprecated since balena CLI v12.0.0',
char: 'l',
}),
'noconvert-eol': flags.boolean({
description:
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).",

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
import type * as BalenaSdk from 'balena-sdk';
import * as semver from 'balena-semver';
import { getBalenaSdk } from './lazy';
import { getBalenaSdk, stripIndent } from './lazy';
export interface ImgConfig {
applicationName: string;
@ -35,6 +35,9 @@ export interface ImgConfig {
wifiKey?: string;
initialDeviceName?: string;
apiKey?: string;
deviceApiKey?: string;
// props for older OS versions
connectivity?: string;
files?: {
@ -51,7 +54,7 @@ export interface ImgConfig {
};
}
export async function generateBaseConfig(
export async function generateApplicationConfig(
application: BalenaSdk.Application,
options: {
version: string;
@ -70,9 +73,7 @@ export async function generateBaseConfig(
const config = (await getBalenaSdk().models.os.getConfig(
application.slug,
options,
)) as ImgConfig & { apiKey?: string };
// os.getConfig always returns a config for an app
delete config.apiKey;
)) as ImgConfig;
// merge sshKeys to config, when they have been specified
if (options.os && options.os.sshKeys) {
@ -86,25 +87,6 @@ export async function generateBaseConfig(
return config;
}
export async function generateApplicationConfig(
application: BalenaSdk.Application,
options: {
version: string;
deviceType?: string;
appUpdatePollInterval?: number;
},
) {
const config = await generateBaseConfig(application, options);
if (semver.satisfies(options.version, '<2.7.8')) {
await addApplicationKey(config, application.id);
} else {
await addProvisioningKey(config, application.id);
}
return config;
}
export function generateDeviceConfig(
device: DeviceWithDeviceType & {
belongs_to__application: BalenaSdk.PineDeferred;
@ -112,19 +94,32 @@ export function generateDeviceConfig(
deviceApiKey: string | true | undefined,
options: { version: string },
) {
return getBalenaSdk()
.models.application.get(device.belongs_to__application.__id)
const sdk = getBalenaSdk();
return sdk.models.application
.get(device.belongs_to__application.__id)
.then(async (application) => {
const baseConfigOpts = {
...options,
deviceType: device.is_of__device_type[0].slug,
};
const config = await generateBaseConfig(application, baseConfigOpts);
// TODO: Generate the correct key beforehand and pass it to os.getConfig() once
// the API supports injecting a provided key, to avoid generating an unused one.
const config = await generateApplicationConfig(
application,
baseConfigOpts,
);
// os.getConfig always returns a config for an app
delete config.apiKey;
if (deviceApiKey == null && semver.satisfies(options.version, '<2.0.3')) {
await addApplicationKey(config, application.id);
config.apiKey = await sdk.models.application.generateApiKey(
application.id,
);
} else {
await addDeviceKey(config, device.uuid, deviceApiKey || true);
config.deviceApiKey =
typeof deviceApiKey === 'string' && deviceApiKey
? deviceApiKey
: await sdk.models.device.generateDeviceKey(device.uuid);
}
return config;
@ -140,34 +135,33 @@ export function generateDeviceConfig(
});
}
function addApplicationKey(config: any, applicationNameOrId: string | number) {
return getBalenaSdk()
.models.application.generateApiKey(applicationNameOrId)
.then((apiKey) => {
config.apiKey = apiKey;
return apiKey;
});
}
function addProvisioningKey(config: any, applicationNameOrId: string | number) {
return getBalenaSdk()
.models.application.generateProvisioningKey(applicationNameOrId)
.then((apiKey) => {
config.apiKey = apiKey;
return apiKey;
});
}
async function addDeviceKey(
config: any,
uuid: string,
customDeviceApiKey: string | true,
/**
* Chech whether the `--dev` option of commands related to OS configuration
* such as `os configure` and `config generate` is compatible with a given
* balenaOS version, and print a warning regarding the consequences of using
* that option.
*/
export async function validateDevOptionAndWarn(
dev?: boolean,
version?: string,
logger?: import('./logger'),
) {
if (customDeviceApiKey === true) {
config.deviceApiKey = await getBalenaSdk().models.device.generateDeviceKey(
uuid,
);
} else {
config.deviceApiKey = customDeviceApiKey;
if (!dev) {
return;
}
if (version && /\bprod\b/.test(version)) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
`Error: The '--dev' option conflicts with production balenaOS version '${version}'`,
);
}
if (!logger) {
const Logger = await import('./logger');
logger = Logger.getLogger();
}
logger.logInfo(stripIndent`
The '--dev' option is being used to configure a balenaOS image in development mode.
Please note that development mode allows unauthenticated, passwordless root ssh access
and exposes network ports such as 2375 that allows unencrypted access to balenaEngine.
Therefore, development mode should only be used in private, trusted local networks.`);
}

View File

@ -17,15 +17,27 @@
import { getVisuals } from './lazy';
import { promisify } from 'util';
import type * as Dockerode from 'dockerode';
import type Logger = require('./logger');
import type { Request } from 'request';
const getBuilderPushEndpoint = function (baseUrl, owner, app) {
const querystring = require('querystring');
const getBuilderPushEndpoint = function (
baseUrl: string,
owner: string,
app: string,
) {
const querystring = require('querystring') as typeof import('querystring');
const args = querystring.stringify({ owner, app });
return `https://builder.${baseUrl}/v1/push?${args}`;
};
const getBuilderLogPushEndpoint = function (baseUrl, buildId, owner, app) {
const querystring = require('querystring');
const getBuilderLogPushEndpoint = function (
baseUrl: string,
buildId: number,
owner: string,
app: string,
) {
const querystring = require('querystring') as typeof import('querystring');
const args = querystring.stringify({ owner, app, buildId });
return `https://builder.${baseUrl}/v1/pushLogs?${args}`;
};
@ -35,32 +47,37 @@ const getBuilderLogPushEndpoint = function (baseUrl, buildId, owner, app) {
* @param {string} imageId
* @param {string} bufferFile
*/
const bufferImage = function (docker, imageId, bufferFile) {
const streamUtils = require('./streams');
const bufferImage = function (
docker: Dockerode,
imageId: string,
bufferFile: string,
): Promise<NodeJS.ReadableStream & { length: number }> {
const streamUtils = require('./streams') as typeof import('./streams');
const image = docker.getImage(imageId);
const sizePromise = image.inspect().then((img) => img.Size);
return Promise.all([image.get(), sizePromise]).then(
([imageStream, imageSize]) =>
streamUtils.buffer(imageStream, bufferFile).then((bufferedStream) => {
// @ts-ignore adding an extra property
bufferedStream.length = imageSize;
return bufferedStream;
}),
streamUtils
.buffer(imageStream, bufferFile)
.then((bufferedStream: NodeJS.ReadableStream & { length?: number }) => {
bufferedStream.length = imageSize;
return bufferedStream as NodeJS.ReadableStream & { length: number };
}),
);
};
const showPushProgress = function (message) {
const showPushProgress = function (message: string) {
const visuals = getVisuals();
const progressBar = new visuals.Progress(message);
progressBar.update({ percentage: 0 });
return progressBar;
};
const uploadToPromise = (uploadRequest, logger) =>
new Promise(function (resolve, reject) {
const handleMessage = function (data) {
const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
new Promise<{ buildId: number }>(function (resolve, reject) {
uploadRequest.on('error', reject).on('data', function handleMessage(data) {
let obj;
data = data.toString();
logger.logDebug(`Received data: ${data}`);
@ -86,25 +103,24 @@ const uploadToPromise = (uploadRequest, logger) =>
default:
reject(new Error(`Received unexpected reply from remote: ${data}`));
}
};
uploadRequest.on('error', reject).on('data', handleMessage);
});
});
/**
* @returns {Promise<{ buildId: number }>}
*/
const uploadImage = function (
imageStream,
token,
username,
url,
appName,
logger,
) {
const request = require('request');
const progressStream = require('progress-stream');
const zlib = require('zlib');
imageStream: NodeJS.ReadableStream & { length: number },
token: string,
username: string,
url: string,
appName: string,
logger: Logger,
): Promise<{ buildId: number }> {
const request = require('request') as typeof import('request');
const progressStream =
require('progress-stream') as typeof import('progress-stream');
const zlib = require('zlib') as typeof import('zlib');
// Need to strip off the newline
const progressMessage = logger
@ -143,8 +159,15 @@ const uploadImage = function (
return uploadToPromise(uploadRequest, logger);
};
const uploadLogs = function (logs, token, url, buildId, username, appName) {
const request = require('request');
const uploadLogs = function (
logs: string,
token: string,
url: string,
buildId: number,
username: string,
appName: string,
) {
const request = require('request') as typeof import('request');
return request.post({
json: true,
url: getBuilderLogPushEndpoint(url, buildId, username, appName),
@ -156,25 +179,24 @@ const uploadLogs = function (logs, token, url, buildId, username, appName) {
};
/**
* @param {import('dockerode')} docker
* @param {import('./logger')} logger
* @param {string} token
* @param {string} username
* @param {string} url
* @param {{appName: string; imageName: string; buildLogs: string; shouldUploadLogs: boolean}} opts
* - appName: the name of the app to deploy to
* - imageName: the name of the image to deploy
* - buildLogs: a string with build output
*/
export const deployLegacy = async function (
docker,
logger,
token,
username,
url,
opts,
) {
const tmp = require('tmp');
docker: Dockerode,
logger: Logger,
token: string,
username: string,
url: string,
opts: {
appName: string;
imageName: string;
buildLogs: string;
shouldUploadLogs: boolean;
},
): Promise<number> {
const tmp = require('tmp') as typeof import('tmp');
const tmpNameAsync = promisify(tmp.tmpName);
// Ensure the tmp files gets deleted
@ -195,8 +217,8 @@ export const deployLegacy = async function (
// has occured before any data was written) this call will throw an
// ugly error, just suppress it
require('fs')
.promises.unlink(bufferFile)
(require('fs') as typeof import('fs')).promises
.unlink(bufferFile)
.catch(() => undefined),
);

View File

@ -34,6 +34,7 @@ import {
loadProject,
makeBuildTasks,
tarDirectory,
makeImageName,
} from '../compose_ts';
import Logger = require('../logger');
import { DeviceAPI, DeviceInfo } from './api';
@ -44,6 +45,7 @@ import { stripIndent } from '../lazy';
const LOCAL_APPNAME = 'localapp';
const LOCAL_RELEASEHASH = 'localrelease';
const LOCAL_PROJECT_NAME = 'local_image';
// Define the logger here so the debug output
// can be used everywhere
@ -57,7 +59,6 @@ export interface DeviceDeployOptions {
registrySecrets: RegistrySecrets;
multiDockerignore: boolean;
nocache: boolean;
nogitignore: boolean; // v13: delete this line
noParentCheck: boolean;
nolive: boolean;
pull: boolean;
@ -182,7 +183,6 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
convertEol: opts.convertEol,
dockerfilePath: opts.dockerfilePath,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore, // v13: delete this line
noParentCheck: opts.noParentCheck,
projectName: 'local',
projectPath: opts.source,
@ -197,14 +197,16 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
await checkBuildSecretsRequirements(docker, opts.source);
globalLogger.logDebug('Tarring all non-ignored files...');
const tarStartTime = Date.now();
const tarStream = await tarDirectory(opts.source, {
composition: project.composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore, // v13: delete this line
});
globalLogger.logDebug(`Tarring complete in ${Date.now() - tarStartTime} ms`);
// Try to detect the device information
globalLogger.logDebug('Fetching device information...');
const deviceInfo = await api.getDeviceInformation();
let buildLogs: Dictionary<string> | undefined;
@ -375,7 +377,11 @@ async function performBuilds(
// We can be sure that localImage.name is set here, because of the failure code above
const image = docker.getImage(localImage.name!);
await image.tag({
repo: generateImageName(localImage.serviceName),
repo: makeImageName(
LOCAL_PROJECT_NAME,
localImage.serviceName,
'latest',
),
force: true,
});
imagesToRemove.push(localImage.name!);
@ -426,7 +432,6 @@ export async function rebuildSingleTask(
composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore, // v13: delete this line
});
const task = _.find(
@ -533,7 +538,7 @@ async function assignDockerBuildOpts(
'io.resin.local.image': '1',
'io.resin.local.service': task.serviceName,
},
t: generateImageName(task.serviceName),
t: getImageNameFromTask(task),
nocache: opts.nocache,
forcerm: true,
pull: opts.pull,
@ -550,8 +555,10 @@ async function assignDockerBuildOpts(
);
}
function generateImageName(serviceName: string): string {
return `local_image_${serviceName}:latest`;
function getImageNameFromTask(task: BuildTask): string {
return !task.external && task.tag
? task.tag
: makeImageName(LOCAL_PROJECT_NAME, task.serviceName, 'latest');
}
export function generateTargetState(
@ -587,6 +594,8 @@ export function generateTargetState(
contract = keyedBuildTasks[name].contract;
}
const task = keyedBuildTasks[name];
services[idx] = {
...defaults,
...opts,
@ -595,7 +604,7 @@ export function generateTargetState(
imageId: idx,
serviceName: name,
serviceId: idx,
image: generateImageName(name),
image: getImageNameFromTask(task),
running: true,
},
};

View File

@ -193,19 +193,7 @@ export async function createClient(
opts: dockerode.DockerOptions,
): Promise<dockerode> {
const Docker = await import('dockerode');
const docker = new Docker(opts);
const { modem } = docker;
// Workaround for a docker-modem 2.0.x bug where it sets a default
// socketPath on Windows even if the input options specify a host/port.
if (modem.socketPath && modem.host) {
if (opts.socketPath) {
modem.host = undefined;
modem.port = undefined;
} else if (opts.host) {
modem.socketPath = undefined;
}
}
return docker;
return new Docker(opts);
}
async function generateConnectOpts(opts: ExtendedDockerOptions) {

View File

@ -89,10 +89,10 @@ export async function sudo(
await executeWithPrivileges(command, stderr, isCLIcmd);
}
export function runCommand<T>(commandArgs: string[]): Promise<T> {
export async function runCommand<T>(commandArgs: string[]): Promise<T> {
const { isSubcommand } =
require('../preparser') as typeof import('../preparser');
if (isSubcommand(commandArgs)) {
if (await isSubcommand(commandArgs)) {
commandArgs = [
commandArgs[0] + ':' + commandArgs[1],
...commandArgs.slice(2),
@ -151,26 +151,8 @@ export async function osProgressHandler(step: InitializeEmitter) {
export async function getAppWithArch(
applicationName: string,
): Promise<ApplicationWithDeviceType & { arch: string }> {
const app = await getApplication(applicationName);
const { getExpanded } = await import('./pine');
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
};
}
// TODO: Drop this. The sdk now has this baked in application.get().
function getApplication(
applicationName: string,
): Promise<ApplicationWithDeviceType> {
// Check for an app of the form `user/application`, and send
// that off to a special handler (before importing any modules)
const match = applicationName.split('/');
const extraOptions: BalenaSdk.PineOptions<BalenaSdk.Application> = {
const { getApplication } = await import('./sdk');
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
$expand: {
application_type: {
$select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'],
@ -185,57 +167,20 @@ function getApplication(
},
},
};
const balena = getBalenaSdk();
if (match.length > 1) {
return balena.models.application.getAppByOwner(
match[1],
match[0],
extraOptions,
) as Promise<ApplicationWithDeviceType>;
}
return balena.models.application.get(
const app = (await getApplication(
balena,
applicationName,
extraOptions,
) as Promise<ApplicationWithDeviceType>;
}
options,
)) as ApplicationWithDeviceType;
const { getExpanded } = await import('./pine');
/**
* Returns the boot partition number of the given image.
* @param imagePath Local filesystem path to a balenaOS image file
*/
export async function getBootPartition(imagePath: string): Promise<number> {
const imagefs = await import('balena-image-fs');
const filedisk = await import('file-disk');
const partitioninfo = await import('partitioninfo');
const partitionNumber = await filedisk.withOpenFile(
imagePath,
'r',
async (handle) => {
const disk = new filedisk.FileDisk(handle, true, false, false);
const { partitions } = await partitioninfo.getPartitions(disk, {
includeExtended: false,
getLogical: true,
});
for (const { index } of partitions) {
try {
return await imagefs.interact(disk, index, async (fs) => {
const statAsync = promisify(fs.stat);
const stats = await statAsync('/device-type.json');
if (stats.isFile()) {
return index;
}
});
} catch (error) {
// noop
}
}
},
);
return partitionNumber ?? 1;
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
};
}
const second = 1000; // 1000 milliseconds
@ -465,7 +410,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
export const expandForAppName = {
$expand: {
belongs_to__application: { $select: 'app_name' },
belongs_to__application: { $select: ['app_name', 'slug'] as any },
is_of__device_type: { $select: 'slug' },
is_running__release: { $select: 'commit' },
},

View File

@ -17,189 +17,60 @@
import * as _ from 'lodash';
import { promises as fs, Stats } from 'fs';
import * as path from 'path';
import * as MultiBuild from 'resin-multibuild';
import dockerIgnore = require('@zeit/dockerignore');
import ignore from 'ignore';
import type { Ignore } from '@balena/dockerignore';
import { ExpectedError } from '../errors';
const { toPosixPath } = MultiBuild.PathUtils;
// v13: delete this enum
export enum IgnoreFileType {
DockerIgnore,
GitIgnore,
}
interface IgnoreEntry {
pattern: string;
// The relative file path from the base path of the build context
filePath: string;
}
/**
* This class is used by the CLI v10 / v11 "original" tarDirectory function
* in `compose.js`. It is still around for the benefit of the `--gitignore`
* option, but is expected to be deleted in CLI v13.
*
* v13: delete this class
*/
export class FileIgnorer {
private dockerIgnoreEntries: IgnoreEntry[];
private gitIgnoreEntries: IgnoreEntry[];
private static ignoreFiles: Array<{
pattern: string;
type: IgnoreFileType;
allowSubdirs: boolean;
}> = [
{
pattern: '.gitignore',
type: IgnoreFileType.GitIgnore,
allowSubdirs: true,
},
{
pattern: '.dockerignore',
type: IgnoreFileType.DockerIgnore,
allowSubdirs: false,
},
];
public constructor(public basePath: string) {
this.dockerIgnoreEntries = [];
this.gitIgnoreEntries = [];
}
/**
* @param {string} relativePath
* The relative pathname from the build context, for example a root level .gitignore should be
* ./.gitignore
* @returns IgnoreFileType
* The type of ignore file, or null
*/
public getIgnoreFileType(relativePath: string): IgnoreFileType | null {
for (const { pattern, type, allowSubdirs } of FileIgnorer.ignoreFiles) {
if (
path.basename(relativePath) === pattern &&
(allowSubdirs || path.dirname(relativePath) === '.')
) {
return type;
}
}
return null;
}
/**
* @param {string} fullPath
* The full path on disk of the ignore file
* @param {IgnoreFileType} type
* @returns Promise
*/
public async addIgnoreFile(
fullPath: string,
type: IgnoreFileType,
): Promise<void> {
const contents = await fs.readFile(fullPath, 'utf8');
contents.split('\n').forEach((line) => {
// ignore empty lines and comments
if (/\s*#/.test(line) || _.isEmpty(line)) {
return;
}
this.addEntry(line, fullPath, type);
});
return;
}
// Pass this function as a predicate to a filter function, and it will filter
// any ignored files
public filter = (filename: string): boolean => {
const relFile = path.relative(this.basePath, filename);
// Don't ignore any metadata files
// The regex below matches `.balena/qemu` and `myservice/.balena/qemu`
// but not `some.dir.for.balena/qemu`.
if (/(^|\/)\.(balena|resin)\//.test(toPosixPath(relFile))) {
return true;
}
// Don't ignore Dockerfile (with or without extension) or docker-compose.yml
if (
/^Dockerfile$|^Dockerfile\.\S+/.test(path.basename(relFile)) ||
path.basename(relFile) === 'docker-compose.yml'
) {
return true;
}
const dockerIgnoreHandle = dockerIgnore();
const gitIgnoreHandle = ignore();
interface IgnoreHandle {
add: (pattern: string) => void;
ignores: (file: string) => boolean;
}
const ignoreTypes: Array<{
handle: IgnoreHandle;
entries: IgnoreEntry[];
}> = [
{ handle: dockerIgnoreHandle, entries: this.dockerIgnoreEntries },
{ handle: gitIgnoreHandle, entries: this.gitIgnoreEntries },
];
_.each(ignoreTypes, ({ handle, entries }) => {
_.each(entries, ({ pattern, filePath }) => {
if (FileIgnorer.contains(path.posix.dirname(filePath), filename)) {
handle.add(pattern);
}
});
});
return !_.some(ignoreTypes, ({ handle }) => handle.ignores(relFile));
}; // tslint:disable-line:semicolon
private addEntry(
pattern: string,
filePath: string,
type: IgnoreFileType,
): void {
const entry: IgnoreEntry = { pattern, filePath };
switch (type) {
case IgnoreFileType.DockerIgnore:
this.dockerIgnoreEntries.push(entry);
break;
case IgnoreFileType.GitIgnore:
this.gitIgnoreEntries.push(entry);
break;
}
}
/**
* Given two paths, check whether the first contains the second
* @param path1 The potentially containing path
* @param path2 The potentially contained path
* @return A boolean indicating whether `path1` contains `path2`
*/
private static contains(path1: string, path2: string): boolean {
// First normalise the input, to remove any path weirdness
path1 = path.posix.normalize(path1);
path2 = path.posix.normalize(path2);
// Now test if the start of the relative path contains ../ ,
// which would tell us that path1 is not part of path2
return !/^\.\.\//.test(path.posix.relative(path1, path2));
}
}
export interface FileStats {
filePath: string;
relPath: string;
stats: Stats;
}
/**
* Create a list of files for the filesystem subtree rooted at
* projectDir, excluding entries for directories themselves.
* @param projectDir Source directory (root of subtree to be listed)
*/
async function listFiles(projectDir: string): Promise<string[]> {
const files: string[] = [];
async function walk(currentDirs: string[]): Promise<string[]> {
if (!currentDirs.length) {
return files;
}
const foundDirs: string[] = [];
// Because `currentDirs` can be of arbitrary length, process them in smaller batches
// to avoid out of memory errors.
// This approach is significantly faster than using Bluebird.map with a
// concurrency setting
const chunks = _.chunk(currentDirs, 100);
for (const chunk of chunks) {
await Promise.all(
chunk.map(async (dir) => {
const _files = await fs.readdir(dir, { withFileTypes: true });
for (const entry of _files) {
const fpath = path.join(dir, entry.name);
const isDirectory =
entry.isDirectory() ||
(entry.isSymbolicLink() && (await fs.stat(fpath)).isDirectory());
if (isDirectory) {
foundDirs.push(fpath);
} else {
files.push(fpath);
}
}
}),
);
}
return walk(foundDirs);
}
return walk([projectDir]);
}
/**
* Return the contents of a .dockerignore file at projectDir, as a string.
* Return an empty string if a .dockerignore file does not exist.
@ -211,7 +82,7 @@ async function readDockerIgnoreFile(projectDir: string): Promise<string> {
let dockerIgnoreStr = '';
try {
dockerIgnoreStr = await fs.readFile(dockerIgnorePath, 'utf8');
} catch (err) {
} catch (err: any) {
if (err.code !== 'ENOENT') {
throw new ExpectedError(
`Error reading file "${dockerIgnorePath}": ${err.message}`,
@ -269,7 +140,10 @@ export async function filterFilesWithDockerignore(
projectDir: string,
multiDockerignore: boolean,
serviceDirsByService: ServiceDirs,
): Promise<{ filteredFileList: FileStats[]; dockerignoreFiles: FileStats[] }> {
): Promise<{
filteredFileList: FileStats[];
dockerignoreFiles: FileStats[];
}> {
// path.resolve() also converts forward slashes to backslashes on Windows
projectDir = path.resolve(projectDir);
const root = '.' + path.sep;
@ -294,45 +168,65 @@ export async function filterFilesWithDockerignore(
const dockerignoreServiceDirs: string[] = multiDockerignore
? Object.keys(ignoreByDir).filter((dir) => dir && dir !== root)
: [];
const files = await listFiles(projectDir);
const dockerignoreFiles: FileStats[] = [];
const filteredFileList: FileStats[] = [];
const klaw = await import('klaw');
await new Promise((resolve, reject) => {
// Looking at klaw's source code, `preserveSymlinks` appears to only
// afect the `stats` argument to the `data` event handler
klaw(projectDir, { preserveSymlinks: false })
.on('error', reject)
.on('end', resolve)
.on('data', (item: { path: string; stats: Stats }) => {
const { path: filePath, stats } = item;
// With `preserveSymlinks: false`, filePath cannot be a symlink.
// filePath may be a directory or a regular or special file
if (!stats.isFile()) {
return;
}
// Because `files` can be of arbitrary length, process them in smaller batches
// to avoid out of memory errors.
// This approach is significantly faster than using Bluebird.map with a
// concurrency setting
const chunks = _.chunk(files, 750);
for (const chunk of chunks) {
await Promise.all(
chunk.map(async (filePath) => {
const relPath = path.relative(projectDir, filePath);
const fileInfo = {
filePath,
relPath,
stats,
};
// .dockerignore files are always added to a list of known dockerignore files
if (path.basename(relPath) === '.dockerignore') {
dockerignoreFiles.push(fileInfo);
const diStats = await fs.stat(filePath);
dockerignoreFiles.push({
filePath,
relPath,
stats: diStats,
});
}
for (const dir of dockerignoreServiceDirs) {
if (relPath.startsWith(dir)) {
if (!ignoreByDir[dir].ignores(relPath.substring(dir.length))) {
filteredFileList.push(fileInfo);
}
// First check if the file is ignored by a .dockerignore file in a service directory
const matchingDir = dockerignoreServiceDirs.find((dir) => {
return relPath.startsWith(dir);
});
// If the file is ignore in a service directory, exit early, otherwise check if it is ignored by the root .dockerignore file.
// Crucially, if the file is in a known service directory, and isn't ignored, the root .dockerignore file should not be checked.
if (matchingDir) {
if (
ignoreByDir[matchingDir].ignores(
relPath.substring(matchingDir.length),
)
) {
return;
}
} else if (ignoreByDir[root].ignores(relPath)) {
return;
}
if (!ignoreByDir[root].ignores(relPath)) {
filteredFileList.push(fileInfo);
// At this point we can do a final stat of the file, and check if it should be included
const stats = await fs.stat(filePath);
// filePath may be a special file that we should ignore, such as a socket
if (stats.isFile()) {
filteredFileList.push({
filePath,
relPath,
stats,
});
}
});
});
}),
);
}
return { filteredFileList, dockerignoreFiles };
}

View File

@ -21,7 +21,6 @@ import type { Chalk } from 'chalk';
import type * as visuals from 'resin-cli-visuals';
import type * as CliForm from 'resin-cli-form';
import type { ux } from 'cli-ux';
import type { stripIndent as StripIndent } from 'common-tags';
// Equivalent of _.once but avoiding the need to import lodash for lazy deps
const once = <T>(fn: () => T) => {
@ -63,4 +62,4 @@ export const getCliUx = once(() => require('cli-ux').ux as typeof ux);
// Directly export stripIndent as we always use it immediately, but importing just `stripIndent` reduces startup time
export const stripIndent =
// tslint:disable-next-line:no-var-requires
require('common-tags/lib/stripIndent') as typeof StripIndent;
require('common-tags/lib/stripIndent') as typeof import('common-tags/lib/stripIndent');

View File

@ -15,8 +15,6 @@
* limitations under the License.
*/
import { isV13 } from './version';
export const reachingOut = `\
For further help or support, visit:
https://www.balena.io/docs/reference/balena-cli/#support-faq-and-troubleshooting
@ -88,60 +86,7 @@ If the --registry-secrets option is not specified, and a secrets.yml or
secrets.json file exists in the balena directory (usually $HOME/.balena),
this file will be used instead.`;
const dockerignoreHelpV12 =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
the project root (--source directory) in order to decide which source files to
exclude from the "build context" (tar stream) sent to balenaCloud, Docker
daemon or balenaEngine. In a microservices (multicontainer) fleet, the
source directory is the directory that contains the "docker-compose.yml" file.
The --multi-dockerignore (-m) option may be used with microservices
(multicontainer) fleets that define a docker-compose.yml file. When this
option is used, each service subdirectory (defined by the \`build\` or
\`build.context\` service properties in the docker-compose.yml file) is
filtered separately according to a .dockerignore file defined in the service
subdirectory. If no .dockerignore file exists in a service subdirectory, then
only the default .dockerignore patterns (see below) apply for that service
subdirectory.
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
any) defined at the overall project root will be used to filter files and
subdirectories other than service subdirectories. It will not have any effect
on service subdirectories, whether or not a service subdirectory defines its
own .dockerignore file. Multiple .dockerignore files are not merged or added
together, and cannot override or extend other files. This behavior maximizes
compatibility with the standard docker-compose tool, while still allowing a
root .dockerignore file (at the overall project root) to filter files and
folders that are outside service subdirectories.
balena CLI releases older than v12.0.0 also took .gitignore files into account.
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
option if compatibility is required. This option is mutually exclusive with
--multi-dockerignore (-m) and will be removed in the CLI's next major version
release (v13).
Default .dockerignore patterns \n` +
`When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
patterns found in the applicable .dockerignore files, in the following order:
\`\`\`
**/.git
< user's patterns from the applicable '.dockerignore' file, if any >
!**/.balena
!**/.resin
!**/Dockerfile
!**/Dockerfile.*
!**/docker-compose.yml
\`\`\`
These patterns always apply, whether or not .dockerignore files exist in the
project. If necessary, the effect of the \`**/.git\` pattern may be modified by
adding counter patterns to the applicable .dockerignore file(s), for example
\`!mysubmodule/.git\`. For documentation on pattern format, see:
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore`;
const dockerignoreHelpV13 =
export const dockerignoreHelp =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
the project root (--source directory) in order to decide which source files to
@ -191,10 +136,6 @@ adding exception patterns to the applicable .dockerignore file(s), for example
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore`;
export const dockerignoreHelp = isV13()
? dockerignoreHelpV13
: dockerignoreHelpV12;
export const applicationIdInfo = `\
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
@ -218,6 +159,18 @@ created public/open fleet, or with fleets from other balena accounts that you
may be invited to join under any role. For this reason, fleet names are
especially discouraged in scripts (e.g. CI environments).`;
export const devModeInfo = `\
The '--dev' option is used to configure balenaOS to operate in development mode,
allowing anauthenticated root ssh access and exposing network ports such as
balenaEngine's 2375 (unencrypted). This option causes \`"developmentMode": true\`
to be inserted in the 'config.json' file in the image's boot partion. Development
mode (as a configurable option) is applicable to balenaOS releases from early
2022. Older releases have separate development and production balenaOS images
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
confuse the balenaOS "development mode" with a device's "local mode", the latter
being a supervisor feature that allows the "balena push" command to push a user's
application directly to a device in the local network.`;
export const jsonInfo = `\
The --json option is recommended when scripting the output of this command,
because field names are less likely to change in JSON format and because it
@ -234,30 +187,6 @@ If you have a particular use for buildArg, which is not satisfied by build-time
secrets, please contact us via support or the forums: https://forums.balena.io/
\n`;
// Note: if you edit this message, check that the regex replace
// logic in lib/commands/apps.ts still works.
export const appToFleetCmdMsg = `\
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
of the balena CLI (so that a different 'app' command can be implemented
in the future). Use 'fleet' instead of 'app' to avoid this warning.
Find out more at: https://git.io/JRuZr`;
export const appToFleetFlagMsg = `\
Renaming notice: The '-a', '--app' or '--application' options are now
aliases for the '-f' or '--fleet' options. THE ALIASES WILL BE REMOVED
in the next major version of the balena CLI (so that a different '--app'
option can be implemented in the future). Use '-f' or '--fleet' instead.
Find out more at: https://git.io/JRuZr`;
export const appToFleetOutputMsg = `\
Renaming notice: The 'app' or 'application' words in table headers
or in JSON object keys/properties will be replaced with 'fleet' in
the next major version of the CLI (v13). The --v13 option may be used
to enable the new names already now, and suppress a warning message.
(The --v13 option will be silently ignored in CLI v13.)
Find out more at: https://git.io/JRuZr`;
export function getNodeEngineVersionWarn(
version: string,
validVersions: string,

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