Compare commits

..

292 Commits

Author SHA1 Message Date
406482b4da v16.7.0 2023-07-17 19:59:52 +00:00
a381c97ca9 Merge pull request #2649 from balena-io/preload-no-pin-device-to-release
preload: Add the --no-pin-device-to-release flag to avoid interactive questions
2023-07-17 19:59:04 +00:00
8ce78ba33c Rerun npm-shrinkwrap.json deduplication 2023-07-17 11:25:02 +03:00
f53f148c89 preload: Add the --no-pin-device-to-release flag to avoid interactive questions
Change-type: minor
See: https://balena.zulipchat.com/#narrow/stream/345746-aspect.2Fproduct/topic/Level.20-.20need.20thoughts.20on.20questions.20.26.20feature.20suggestions
2023-07-17 11:19:03 +03:00
0086feb645 v16.6.6 2023-07-10 17:16:08 +00:00
4ee55b049f Merge pull request #2646 from balena-io/reduce-lodash-usage
Reduce lodash usage in common user interaction patterns
2023-07-10 17:15:14 +00:00
90c6f121cc Rerun npm-shrinkwrap.json deduplication 2023-07-10 19:36:36 +03:00
d3c27ae859 Reduce lodash usage in common user interaction patterns
Change-type: patch
2023-07-10 17:19:01 +03:00
8f39c1de6c v16.6.5 2023-07-09 21:29:55 +00:00
4df1831187 Merge pull request #2645 from balena-io/application-create-hostApp-based-supported-DTs
fleet/block/app create: Fetch the supported device types using the hostApps
2023-07-09 21:29:02 +00:00
2bce761ace Rerun npm-shrinkwrap.json deduplication 2023-07-07 20:17:53 +03:00
d78b76aceb fleet/block/app create: Fetch the supported device types using the hostApps
Change-type: patch
See: https://balena.zulipchat.com/#narrow/stream/360838-balena-io.2Fos.2Fdevices/topic/state.20field.20in.20device-type.2Ejson
See: https://balena.fibery.io/Organisation/Improvements-849#Improvements/Stop-relying-on-device-types-v1-device-type.json-for-unrelated-things-257
2023-07-07 19:57:36 +03:00
f07f6b84d4 v16.6.4 2023-07-06 13:58:27 +00:00
d297a10570 Merge pull request #2643 from balena-io/bump-balena-compose
Bump balena-compose to v2.3.0
2023-07-06 13:57:37 +00:00
9d0b82122a Bump balena-compose to v2.3.0
This allows the the CLI to use docker registry config when querying the
images manifest.

Relates-to: balena-io-modules/balena-compose#31
Change-type: patch
2023-07-05 15:46:42 -04:00
338477463a v16.6.3 2023-06-30 17:07:34 +00:00
d1275760fa Merge pull request #2641 from balena-io/drop-toolbelt
Pin dockerode and drop docker-toolbelt
2023-06-30 20:06:46 +03:00
0f4054fa4d Remove redundant dependency on docker-toolbelt
Change-type: patch
2023-06-30 19:22:07 +03:00
7545fc5d6e Pin dockerode to v3.3.3
v3.3.4 introduces a regression that is fixed by https://github.com/apocas/dockerode/pull/695 but Dockerode has not published a version that includes the fix yet. Pin the dependency to ensure we don’t ever update to a broken version.

Change-type: patch
2023-06-30 19:19:52 +03:00
a1f25809cb v16.6.2 2023-06-29 12:27:14 +00:00
e0a3c4bd95 Merge pull request #2640 from balena-io/make-apple-notarization-team-id-public
macos notarization: Expose team ID instead of keeping it in secrets
2023-06-29 12:26:27 +00:00
d843e75512 macos notarization: Expose team ID instead of keeping it in secrets
Change-type: patch
2023-06-29 07:41:07 -04:00
72c57608d5 v16.6.1 2023-06-28 12:47:56 +00:00
d9de7636db Merge pull request #2639 from balena-io/update-electron-notarize
Drop `electron-notarize` dependency in favor of `@electron/notarize`
2023-06-28 12:47:13 +00:00
10b5af6967 Drop electron-notarize dependency in favor of @electron/notarize
Change-type: patch
2023-06-28 08:01:39 -04:00
51c050c725 v16.6.0 2023-06-26 13:05:11 +00:00
eb52c47de5 Merge pull request #2631 from balena-io/add-api-key-revoke-command
api-key: Add `revoke` command which accepts a list of API key ids
2023-06-26 09:04:21 -04:00
4b1378dfbc api-key: Add revoke command which accepts a list of API key ids
Change-type: minor
2023-06-26 08:21:23 -04:00
1a77d86347 v16.5.2 2023-06-09 16:03:10 +00:00
bd5188f4b9 Merge pull request #2637 from balena-io/klutchell-patch-2
Re-enable automatic github final releases
2023-06-09 16:02:22 +00:00
034f459bfa Update npm-shrinkwrap
Signed-off-by: Kyle Harding <kyle@balena.io>
2023-06-09 11:23:49 -04:00
bc405d997e Re-enable automatic github final releases
Change-type: patch
2023-06-09 11:05:01 -04:00
af27cf2cbe v16.5.1 2023-06-02 06:30:00 +00:00
83b9bf67c2 Merge pull request #2636 from balena-io/bump-ts
Update TypeScript to 5.1.3
2023-06-02 06:29:11 +00:00
abd73b805b Update TypeScript to 5.1.3
Change-type: patch
2023-06-01 21:58:34 +03:00
37bfd4db98 v16.5.0 2023-05-25 16:45:02 +00:00
be74143d5f Merge pull request #2633 from balena-io/add-block-create-command
Add `balena block create` command for creating Blocks
2023-05-25 16:44:19 +00:00
9975e5d9ac Add balena block create command for creating Blocks
Change-type: minor
2023-05-25 12:00:39 -04:00
1341413966 v16.4.0 2023-05-25 15:53:23 +00:00
1a5b914a6f Merge pull request #2632 from balena-io/add-app-create-command
Add `balena app create` command for creatings Apps
2023-05-25 15:52:37 +00:00
c5e8f0d6ea Add balena app create command for creatings Apps
Change-type: minor
2023-05-25 11:09:02 -04:00
3a143fe413 v16.3.0 2023-05-25 15:06:39 +00:00
3445e4a08e Merge pull request #2630 from balena-io/add-command-to-list-user-api-keys
Add `balena api-keys` command for listing user API keys
2023-05-25 15:05:38 +00:00
166130c3df Add balena api-keys command for listing user/fleet API keys
Change-type: minor
2023-05-25 09:11:36 -04:00
c3a8a905f7 v16.2.7 2023-05-24 12:04:59 +00:00
2b878e87d8 Merge pull request #2629 from balena-io/less-requests-2
Fetch the application and its devices in one request in balena devices, device init, ssh, tunnel
2023-05-24 12:04:05 +00:00
063e9d40f0 device init: Avoid extra request when not providing the --fleet option
Change-type: patch
2023-05-24 14:22:04 +03:00
2b58143164 devices: Use a single request when providing the --fleet parameter
Reduces the response time when using
--fleet from 1.5s to 1s.

Change-type: patch
2023-05-24 14:22:04 +03:00
861d4f33b7 ssh,tunnel: Fetch the fleet & devices in one request
Change-type: patch
2023-05-24 14:22:04 +03:00
81f4aae7d2 v16.2.6 2023-05-24 11:17:30 +00:00
46ab335407 Merge pull request #2626 from balena-io/less-requests
Stop fetching unnecessary fields
2023-05-24 14:16:42 +03:00
07cb0cbfcd device init: Stop fetching unnecessary app fields
Change-type: patch
2023-05-24 13:37:19 +03:00
a2392dc580 device move: Stop fetching unnecessary device & app fields
Reduces the amount of device data retrieved
by 66%.

Change-type: patch
2023-05-24 13:37:19 +03:00
8b3235ab2b device register: Stop fetching unnecessary app fields
Change-type: patch
2023-05-24 13:37:19 +03:00
15dac6f194 config generate: Stop fetching unnecessary app fields
Change-type: patch
2023-05-24 13:37:19 +03:00
3c93db8449 devices: Stop fetching unnecessary device fields
Change-type: patch
2023-05-24 13:37:19 +03:00
9d8df0b781 fleet rm,restart,rename,purge: Stop fetching unnecessary app fields
Halves the amout of application data retrieved.

Change-type: patch
2023-05-24 13:37:19 +03:00
bcadbdbed8 push: Stop unnecessarily fetching the application name
Change-type: patch
2023-05-24 13:37:19 +03:00
05a96fa60e ssh,tunnel: Reduce the amount of application fields fetched
Halves the amout of application data retrieved.

Change-type: patch
2023-05-24 13:37:19 +03:00
2e37536e7a orgs: Stop fetching unnecessary org fields
Halves the amount of org data retrieved to
show the list of orgs to select from.

Change-type: patch
2023-05-24 13:37:19 +03:00
025c4ef7f2 fleet create: Reduce the amount of org fields fetched
Halves the amount of org data retrieved to
show the list of orgs to select from.

Change-type: patch
2023-05-24 13:37:19 +03:00
ecbc660bf5 Fetch only the app slug when resolving an app by name
Affects many env & tags commands, as well
as support, releases & preload.

Change-type: patch
2023-05-24 13:37:19 +03:00
ba1f17d537 v16.2.5 2023-05-24 10:30:21 +00:00
3ab8f7500e Merge pull request #2628 from balena-io/73-fix-device-init-os-initialize
Fix device int & os initialize failing to initialize the drive list
2023-05-24 13:29:36 +03:00
0a25bec010 Fix device int & os initialize failing to initialize the drive list
Resolves: #2627
Change-type: patch
2023-05-24 12:47:06 +03:00
01e765e670 v16.2.4 2023-05-23 20:31:27 +00:00
61844f2386 Merge pull request #2625 from balena-io/app-select-reduce-requests
Remove extra request when filling the application selection list
2023-05-23 20:30:32 +00:00
46aa08c953 Remove extra request when filling the application selection list
Saves one request of about 450ms on the
init and move commands.

Change-type: patch
2023-05-23 22:47:10 +03:00
b6c7fb82c3 v16.2.3 2023-05-23 19:46:47 +00:00
fcda09009a Merge pull request #2624 from balena-io/improve-typings
Use stricter typings
2023-05-23 19:46:02 +00:00
1a6fe1f3de Use stricter typings
Change-type: patch
2023-05-23 18:57:54 +03:00
98e91c0607 v16.2.2 2023-05-23 14:02:36 +00:00
bed2387d83 Merge pull request #2623 from balena-io/fix-setting-service-env-vars-by-app-name
env add: Fix accepting fleet names when setting service vars
2023-05-23 14:01:21 +00:00
50e852acee env add: Fix accepting fleet names when setting service vars
Change-type: patch
2023-05-23 15:59:41 +03:00
da30623e4e v16.2.1 2023-05-23 12:28:25 +00:00
7a46b367a7 Merge pull request #2621 from balena-io/sdk-v17
Update balena-sdk to 17.0.0
2023-05-23 12:27:29 +00:00
d9651c7393 Update balena-settings-client to 5.0.2
Change-type: patch
2023-05-23 13:22:38 +03:00
e371b1e759 Update balena-preload & balena-image-manager
Update balena-image-manager from 8.0.1 to 9.0.0
Update balena-preload from 13.0.0 to 14.0.0

Change-type: patch
2023-05-23 13:22:38 +03:00
77cf4af166 Update balena-sdk to 17.0.0
Update balena-sdk from 16.45.1 to 17.0.0

Change-type: patch
2023-05-23 13:22:38 +03:00
9d197317ca v16.2.0 2023-05-19 18:11:57 +00:00
9a8b0b4a0d Merge pull request #2619 from balena-io/alexgg/sb
os configure, config generate: Add '--secureBoot' option to opt-in secure boot
2023-05-19 18:11:02 +00:00
0c62b9ef08 Deduplicate npm-shrinkwra.json 2023-05-19 20:28:12 +03:00
83a5e7392a secureboot: Retrieve the OS release & contract in one request
Change-type: patch
2023-05-19 19:22:23 +03:00
f0c8c37022 os configure, config generate: Add '--secureBoot' option to opt-in secure boot
Allow to generate a config file with `installer.secureboot` set so that
a secure boot and disk encrypted system can be installed.

Change-type: minor
Signed-off-by: Alex Gonzalez <alexg@balena.io>
2023-05-19 18:10:00 +02:00
ba26d3204d package.json: Update balena-sdk to 16.44.2
Update balena-sdk from 16.40.0 to 16.44.2

Change-type: patch
Signed-off-by: Alex Gonzalez <alexg@balena.io>
2023-05-19 18:10:00 +02:00
d53542975e flowzone: update custom runs to use macos-12
After the flowzone update to use zstd as compression algorithm for sources
there is an error on macos-11 as tar does not support it.

Change-type: patch
Signed-off-by: Alex Gonzalez <alexg@balena.io>
2023-05-19 11:33:31 +02:00
632296a271 v16.1.0 2023-05-16 18:26:10 +00:00
3e089fcdb2 Merge pull request #2616 from balena-io/ab77/operational
build linux/arm packages
2023-05-16 18:25:27 +00:00
d61c300750 build linux/arm packages
change-type: minor
2023-05-16 10:39:00 -07:00
a0a97c5f40 v16.0.0 2023-05-16 00:02:37 +00:00
165f3b83ca Merge pull request #2618 from balena-io/node-16
Update to Node 16
2023-05-16 00:01:48 +00:00
5bf95300ee support: Change the printed support expiry date in ISO 8601 UTC format
Change-type: major
2023-05-12 19:00:10 +03:00
adb460b270 logs: Change the timestamp format to ISO 8601 UTC
Resolves: #2608
Change-type: major
2023-05-12 19:00:10 +03:00
ca80bd52fe Pin flowzone to v4.7.1
The macos-11 runners apparently do not support zst compression format as
added in flowzone 4.7.2. While support is rolled out, we can keep
the flowzone branch to the previous working version.

Change-type: patch
2023-05-11 19:10:10 -04:00
281f8abb9a Update etcher-sdk to v8.5.3
This removes the dependency on our custom fork of [node-usb](https://github.com/balena-io-modules/node-usb)
and uses the maintained building method of the official node-usb repo

Change-type: patch
2023-05-11 18:10:52 -04:00
2cf2918d73 Update vercel/pkg to v5.8.1
This seems to be needed to build the binaries for node v16 since earlier
versions failed with

```
Error: Could not detect abi for version 16.13.0 and runtime node.  Updating "node-abi" might help solve this issue if it is a new release of node
```

Change-type: patch
2023-05-11 17:49:49 -04:00
7dfb7474f5 Update to Node 16
This also drops support for Node 14

Change-type: major
2023-05-11 17:49:37 -04:00
6ee0b48c9a v15.2.3 2023-05-03 20:03:49 +00:00
bd01fbf90c Merge pull request #2614 from balena-io/local-release-uuid
Use valid release uuid for local releases
2023-05-03 20:02:59 +00:00
cd19845b6b Use valid release uuid for local releases
On local push, the CLI uses `localrelease` as the `commit` property for
the development application. This is not a valid uuid and will not be
read properly by the supervisor, as seen in

https://github.com/balena-os/balena-supervisor/blob/master/src/compose/service.ts#L652

While this is not a problem right now, the commit is becoming the main
way to identify a service release (replacing `releaseId` and `imageId`),
and the invalid release uuid could cause update issues when pushing a
local release on when using some API endpoints.

Change-type: patch
Relates-to: balena-os/balena-supervisor#2136
2023-05-03 15:08:19 -04:00
5545883c3f v15.2.2 2023-04-28 16:16:44 +00:00
75a380b0ba Merge pull request #2615 from balena-io/remove-nvmrc
Remove nvmrc
2023-04-28 16:15:57 +00:00
35fe7c6a58 Remove nvmrc
There is not a lot of benefit to using `.nvmrc` as it still requires
`nvm use`, and not everybody uses `nvm`. The call to `npm install` will
already warn about using the wrong version.

Change-type: patch
2023-04-28 10:27:15 -04:00
69249b3139 v15.2.1 2023-04-28 09:25:50 +00:00
bf897fd56d Merge pull request #2612 from balena-io/sync-tslib
Fix tslib going out of sync causing HUP to fail
2023-04-28 09:25:01 +00:00
150c6e75f5 Fix tslib going out of sync causing HUP to fail
Change-type: patch
2023-04-27 14:07:57 +03:00
e8bc43dc64 v15.2.0 2023-04-05 13:09:24 +00:00
1213689de2 Merge pull request #2606 from balena-io/update-balena-sdk-16.40.0
Add support for device restarts in open-balena
2023-04-05 13:08:14 +00:00
c1017e8e27 Add support for device restarts in open-balena
Update balena-sdk from 16.28.2 to 16.40.0

Change-type: minor
2023-04-05 12:57:33 +03:00
7ad9e685f6 v15.1.3 2023-04-05 08:06:56 +00:00
c778aaffaf Merge pull request #2607 from balena-io/update-balena-sdk-16.28.2
devices supported: Fix showing types without a valid & finalized release
2023-04-05 08:06:05 +00:00
b98047cacf devices supported: Fix showing types without a valid & finalized release
Update balena-sdk from 16.28.0 to 16.28.2

Resolves: #2524
Change-type: patch
2023-04-05 10:19:39 +03:00
03ace6e4b2 v15.1.2 2023-03-27 15:14:47 +00:00
9b4701bcb7 Merge pull request #2601 from balena-io/use-satisfies
Improve type checking by using the satisfies operator
2023-03-27 18:13:56 +03:00
174312977a Improve type checking by using the satisfies operator
Change-type: patch
2023-03-27 16:39:09 +03:00
963d9af817 v15.1.1 2023-03-17 10:20:03 +00:00
af5ec51232 Merge pull request #2600 from balena-io/bump-ts
Update TypeScript to 5.0.2
2023-03-17 12:19:13 +02:00
1cd9fbf6a0 Update TypeScript to 5.0.2
Change-type: patch
2023-03-16 20:53:08 +02:00
72639e9e59 v15.1.0 2023-03-14 20:19:08 +00:00
447dcc1480 Merge pull request #2599 from balena-io/kyle/balena-compose-v2.2.x
Update balena-compose to v2.2.1
2023-03-14 16:18:19 -04:00
564716faa7 Update balena-compose to v2.2.1
Update balena-compose from 2.1.1 to 2.2.1

Change-type: minor
Signed-off-by: Kyle Harding <kyle@balena.io>
2023-03-14 14:59:52 -04:00
3e5b4457c2 v15.0.6 2023-03-13 14:03:48 +00:00
793e70d909 Merge pull request #2597 from balena-io/explicitly-select-devices-fields
Devices: explicitly fetches only used fields
2023-03-13 11:02:51 -03:00
5761a306be Devices: explicitly fetches only used fields
Change-type: patch
2023-03-13 09:35:43 -03:00
adff0f2a0a v15.0.5 2023-03-10 16:25:40 +00:00
4ec45a0c43 Merge pull request #2596 from balena-io/fix-is-legacy-check
Fix isLegacy check which should always relay on the slug
2023-03-10 18:24:48 +02:00
ecf4b046b5 Fix application isLegacy check for rename and deploy
Change-type: patch
2023-03-10 16:33:00 +01:00
b0cae93ac9 v15.0.4 2023-02-21 07:24:19 +00:00
53b66678d4 Merge pull request #2583 from balena-io/hraftery-patch-1-1
Clarify update rate of update-notifier info
2023-02-21 09:23:30 +02:00
0b9b65ef88 patch: Clarify update rate of update notifier info
If the cli has not been run in a while, it will show old update information. It's not obvious why, and this might lead to confusion. So this commit just adds a comment to clarify that out-of-date update notifier info is expected behaviour, and why.
2023-01-26 14:15:43 +11:00
8a84d9d792 v15.0.3 2023-01-18 16:16:39 +00:00
c535b8e1ea Merge pull request #2582 from balena-io/https-npm
Use https for the npm deprecation check, avoiding a redirect
2023-01-18 16:15:01 +00:00
234fb6cd39 Use https for the npm deprecation check, avoiding a redirect
Change-type: patch
2023-01-18 13:11:31 +00:00
8714830b48 v15.0.2 2023-01-14 07:35:13 +00:00
0e07b36691 Merge pull request #2580 from balena-io/joshbwlng/fix-typo
Fix push --nolive doc typo
2023-01-14 09:33:56 +02:00
ba80d3c38c Fix push --nolive doc typo
Change-type: patch
2023-01-13 13:36:44 +09:00
e65dc82cfe v15.0.1 2023-01-10 13:43:24 +00:00
bc727521c6 Merge pull request #2571 from balena-io/nodejs-14
Update to Node 14
2023-01-10 08:41:54 -05:00
a8c0c884d3 Extra linting 2023-01-03 16:08:10 -03:00
b11c7157d3 Update to node 14 2023-01-03 16:04:24 -03:00
578de7bcd4 Process livepush build logs inline
When using livepush, the CLI parses the build logs to obtain the stage
image ids, which are necessary for properly running livepush.

This process used to store the full log output in memory before parsing
the logs for obtaining the stage ids. We have seen this cause issues
before because of the excessive memory usage and it is one the suspects
of #2165, which is blocking the update to Node 14

Change-type: patch
2023-01-03 12:29:54 -03:00
cfc6b3ce9e v15.0.0 2023-01-02 15:21:59 +00:00
1c7a354fe7 Merge pull request #2573 from balena-io/balena-preload-13
Upgrade balena-preload to 13.0.0
2023-01-02 10:20:01 -05:00
40a0941ca3 preload: Drops ability to preload Intel Edison (EOL 2017)
Upgrade balena-preload from 12.2.0 to 13.0.0

Change-type: major
Signed-off-by: Edwin Joassart <edwin.joassart@balena.io>
2023-01-02 15:34:32 +01:00
0ab4760272 v14.5.18 2022-12-29 07:20:50 +00:00
42b2269e81 Merge pull request #2576 from balena-io/flowzone-npm-ci
Update flowzone tests to use npm ci
2022-12-29 02:19:24 -05:00
c818d846b3 Update flowzone tests to use npm ci
Will also make sure that the shrinkwrap is
matching the committed package.json.

Change-type: patch
2022-12-29 08:24:24 +02:00
3328f40416 v14.5.17 2022-12-28 23:56:12 +00:00
58d10c1908 Merge pull request #2575 from balena-io/drop-balena-sync
Stop using the deprecated balena-sync module
2022-12-29 01:54:48 +02:00
2fd0ca6a02 Stop using the deprecated balena-sync module
Change-type: patch
2022-12-29 01:05:51 +02:00
173028fd0d v14.5.16 2022-12-28 23:01:25 +00:00
62d5bf4436 Merge pull request #2574 from balena-io/align-package-json-shrinkwrap
Update the npm-shrinkwrap.json dependencies to match the package.json
2022-12-29 01:00:11 +02:00
63a0d19770 Update the npm-shrinkwrap.json dependencies to match the package.json
Change-type: patch
2022-12-28 21:22:48 +02:00
8244636bf2 v14.5.15 2022-12-12 13:41:15 +00:00
6a01fb361c Merge pull request #2570 from balena-io/aethernet-preload-12.2.0
patch: update balena-preload to 12.2.0
2022-12-12 08:39:46 -05:00
ca637b3fb6 patch: update balena-preload to 12.2.0 2022-12-12 13:16:22 +01:00
006293bd01 v14.5.14 2022-12-11 21:46:38 +00:00
338b5d79d3 Merge pull request #2535 from balena-io/multicast-dns-bump
Bump multicast-dns to rebased commit (again)
2022-12-11 16:45:14 -05:00
60dd0daae5 Bump multicast-dns to rebased commit (again)
A recent PR reverted the multicast-dns commit bump from PR #2401. This means that
under some conditions, `npm install` will fail.

See: https://github.com/balena-io-modules/multicast-dns/pull/1
See: https://github.com/balena-io/balena-cli/pull/2401

Change-type: patch
2022-12-11 12:45:49 -08:00
662b8283a6 v14.5.13 2022-12-08 14:00:28 +00:00
cfc866cf41 Merge pull request #2569 from balena-io/gh-runners
Specify gh runner versions for compatibility reasons
2022-12-08 13:58:56 +00:00
e566badfff Build on macos-11 for library compatibility reasons
Change-type: patch
2022-12-08 10:58:40 +00:00
69834c417e Build on ubuntu-20.04 for library compatibility reasons
Change-type: patch
2022-12-08 10:58:25 +00:00
8aa9c62afd v14.5.12 2022-11-21 18:46:49 +00:00
4f29e37fe7 Merge pull request #2565 from balena-io/ab77/operational
Move GH publishing to FZ core
2022-11-21 18:45:25 +00:00
99e8a36bb5 Move GH publishing to FZ core
Change-type: patch
2022-11-21 09:48:09 -08:00
669cbe227f v14.5.11 2022-11-17 18:32:48 +00:00
e9156d77f1 Merge pull request #2532 from balena-io/nvmrc
Adding .nvmrc so we can use nvm use instead of hunting for version
2022-11-17 18:31:28 +00:00
767216c842 Adding .nvmrc so we can use nvm use instead of hunting for version
Change-type: patch
2022-11-16 17:54:42 -08:00
d3018f9061 v14.5.10 2022-11-11 11:24:21 +00:00
37c6ad855b Merge pull request #2557 from balena-io/surface-sdk-incompatible-dt-errors
Surface sdk incompatible dt errors
2022-11-11 11:23:05 +00:00
ca97678358 Fix surfacing incompatible device type errors as not recognized
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-11-10 16:52:14 -08:00
3bb0036ba8 v14.5.9 2022-11-11 00:49:26 +00:00
ac9e2a9e7e Merge pull request #2562 from balena-io/ab77/operational
Prevent git from existing with 141
2022-11-11 00:47:34 +00:00
52e95e6d0a Prevent git from existing with 141
Change-type: patch
2022-11-10 15:52:13 -08:00
c5d2aa7eec v14.5.8 2022-11-10 23:32:21 +00:00
683220e303 Merge pull request #2561 from balena-io/ab77/operational
Replace missing input
2022-11-10 23:30:51 +00:00
44f09b32fa Replace missing input
Change-type: patch
2022-11-10 14:33:13 -08:00
d1a0660a3d v14.5.7 2022-11-10 22:19:22 +00:00
ee1987f188 Merge pull request #2560 from balena-io/ab77/operational
Just ignore errors during publish
2022-11-10 22:17:59 +00:00
39e9997d9e Just ignore errors during publish
Change-type: patch
2022-11-10 13:22:29 -08:00
97b8c75043 v14.5.6 2022-11-10 21:07:35 +00:00
7cb8349f29 Merge pull request #2559 from balena-io/ab77/operational
Ignore PIPE signal
2022-11-10 21:06:18 +00:00
6063f4c776 Ignore PIPE signal
Change-type: patch
2022-11-10 12:13:03 -08:00
4899d545f1 v14.5.5 2022-11-10 20:07:12 +00:00
115bf6433d Merge pull request #2558 from balena-io/ab77/operational
Don't pipefail
2022-11-10 20:05:33 +00:00
e5ce1ade89 Don't pipefail
Change-type: patch
2022-11-10 11:13:37 -08:00
9c4174ea8a v14.5.4 2022-11-10 18:31:21 +00:00
cf16957195 Merge pull request #2556 from balena-io/2537-error-on-incompatible-resolved-device-types
Error when the device type and image parameters do not match
2022-11-10 18:30:05 +00:00
4de369ff95 Error when the device type and image parameters do not match
Resolves: #2537
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-11-10 09:37:22 -08:00
ac3ebff8ee v14.5.3 2022-11-10 17:20:19 +00:00
76b01d92d3 Merge pull request #2555 from balena-io/ab77/operational
Switch to Flowzone
2022-11-10 17:18:49 +00:00
19144163ee Switch to Flowzone
Change-type: patch
2022-11-08 20:56:47 -08:00
535ffccbad v14.5.2 2022-10-21 20:15:35 +03:00
6f5ada9692 Merge pull request #2553 from balena-io/stop-waiting-for-the-analytics-response
Stop waiting for the analytics response
2022-10-21 17:09:08 +00:00
1c7d9255ae Stop waiting for the analytics response
Change-type: patch
See: https://balena.zulipchat.com/#narrow/stream/345884-aspect.2Fanalytics/topic/Balena.20CLI.20analytics-performance
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-10-21 19:07:39 +03:00
807e6ea2ad v14.5.1 2022-10-21 15:48:13 +03:00
c76f019fd0 Merge pull request #2552 from balena-io/bump-parse-link-header-2.0.0
Bump parse-link-header from 1.0.1 to 2.0.0
2022-10-21 12:45:48 +00:00
3c2c925eed Bump parse-link-header from 1.0.1 to 2.0.0
Bumps [parse-link-header](https://github.com/thlorenz/parse-link-header) from 1.0.1 to 2.0.0.
- [Release notes](https://github.com/thlorenz/parse-link-header/releases)
- [Commits](https://github.com/thlorenz/parse-link-header/compare/v1.0.1...v2.0.0)

---
updated-dependencies:
- dependency-name: parse-link-header
  dependency-type: direct:development
...

Change-type: patch
Signed-off-by: dependabot[bot] <support@github.com>
2022-10-20 20:10:53 +03:00
14b54be15e v14.5.0 2022-10-18 15:17:13 +03:00
7fb82f7447 Merge pull request #2539 from balena-io/send-tracking-to-analytics-backend
changes analytics endpoint to analytics-backend
2022-10-18 12:14:05 +00:00
4a5d44a0f1 Merge branch 'master' into send-tracking-to-analytics-backend 2022-10-18 08:15:33 -03:00
1cba0284df v14.4.4 2022-10-18 13:36:52 +03:00
6e4fe229bf Merge pull request #2546 from balena-io/update-simple-git
Update simple git
2022-10-18 10:33:17 +00:00
7033075900 Update simple-git to 3.14.1
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-10-18 09:45:24 +03:00
ded268ff3c automation/check-doc: Convert to typescript 2022-10-18 09:45:24 +03:00
a366f0b7eb automation/check-doc: Rename to .ts 2022-10-18 09:45:24 +03:00
507c8a1bfd v14.4.3 2022-10-18 00:24:29 +03:00
1fb46bfa5d Merge pull request #2545 from balena-io/config-generate-incompatible-dt-error
config generate: Fix the incompatible arch errors showing as not found
2022-10-17 21:20:21 +00:00
2e115968d5 config generate: Fix the incompatible arch errors showing as not found
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-10-17 23:44:08 +03:00
83020797b0 v14.4.2 2022-10-17 21:15:25 +03:00
0c4647e980 Merge pull request #2544 from balena-io/no-device-type-json-arch-aliases
Stop relying on device-type.json for resolving the cpu architecture
2022-10-17 18:06:05 +00:00
a20d2a04a8 Stop relying on device-type.json for resolving the device type aliases
Resolves: #2541
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-10-17 19:09:09 +03:00
57b0dccc7d Stop relying on device-type.json for resolving the cpu architecture
Resolves: #2542
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-10-17 19:09:09 +03:00
d1e3bdf29a keeps events loggiging with default message
change-type: minor
2022-10-17 10:07:51 -03:00
bdf7fedd7a uses amplitude data events format
Change-type: minor
2022-10-14 10:50:12 -03:00
c163662f4a changes analytics endpoint to analytics-backend
change-type: minor
2022-10-13 19:32:55 -03:00
a2823fd3ec v14.4.1 2022-10-12 18:19:30 +03:00
d717352b84 Merge pull request #2530 from balena-io/hraftery-patch-1
Add to description that command is device specific
2022-10-12 14:59:41 +00:00
e46902e683 balena os initialize: Clarify that the process includes flashing
Change-type: patch
2022-10-12 16:45:16 +03:00
e96ef6697e v14.4.0 2022-10-12 16:37:05 +03:00
6f54197b7b Merge pull request #2533 from balena-io/2531-device-register-dt-param
device register: Add support for the `--deviceType` option
2022-10-12 13:31:08 +00:00
34b4ac2d9f device register: Add support for the --deviceType option
Resolves: #2531
Change-type: minor
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-10-12 11:15:00 +03:00
f99244603a Update balena-sdk to 16.28.0
Update balena-sdk from 16.22.0 to 16.28.0

Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-10-12 11:15:00 +03:00
523c0af0fb v14.3.1 2022-09-06 08:49:41 -04:00
2206b475c6 Merge pull request #2526 from balena-io/unified-os-release-examples
Add unified OS versions in the examples of the device & os commands
2022-09-06 12:48:06 +00:00
a117dc0382 Add unified OS versions in the examples of the device & os commands
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-09-06 13:26:26 +03:00
cf3e8ff909 v14.3.0 2022-08-17 15:26:40 +03:00
36d1af1e33 Merge pull request #2523 from balena-io/add-release-validate-and-invalidate-commands
release: Add `invalidate` and `validate` commands for invalidating and validating releases (respectively)
2022-08-17 12:24:14 +00:00
18f83092fe v14.2.0 2022-08-16 23:32:01 +03:00
ee3c796787 Merge pull request #2522 from balena-io/add-fleet-pin-and-track-latest-commands
Add fleet `pin` and `track-latest` commands
2022-08-16 20:29:51 +00:00
934c3ddf38 release: Add validate command for validating releases
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-15 19:44:54 +00:00
66e6daf78c release: Add invalidate command for invalidating releases
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-15 19:41:25 +00:00
97eb107de4 fleet: Add track-latest command for tracking the latest release
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-15 15:34:54 -04:00
def205f1fb fleet: Add pin command for pinning fleets to a specific release
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-15 15:06:10 -04:00
5c8f78678b v14.1.0 2022-08-04 17:52:53 +04:00
769f1ca5b4 Merge pull request #2493 from balena-io/add-device-track-command
Add device track command
2022-08-04 13:51:04 +00:00
cb26a736fc Add device track command for pinning a device to the latest release or a specific release
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-03 14:42:25 -04:00
d28847d5aa v14.0.0 2022-08-02 19:08:30 +04:00
c0902bb119 Merge pull request #2514 from balena-io/v14
Release v14
2022-08-02 15:05:58 +00:00
26aae0afab v13.10.1 2022-08-02 02:07:46 +04:00
5f3cf75c1a Merge pull request #2516 from balena-io/2515-fix-balena-deploy-jsesc-dependency
Fix balena deploy missing dependency error
2022-08-01 22:05:55 +00:00
8a7fbdb55d Drop undocumented support for numeric ids in balena device commands
Change-type: major
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-01 18:52:30 +00:00
b260f80bcc Drop support for the deprecated balena device public-url <enable|disable|status> <uuid> and related format
Resolves: #2501
Change-type: major
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-01 18:52:30 +00:00
9ec37975f3 Drop support for numeric fleet id parameters from all commands
Resolves: #2500
Change-type: major
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-01 18:52:25 +00:00
73c487c2f5 Fix balena deploy missing dependency error
Resolves: #2515
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-08-01 19:35:07 +03:00
3cb35ea318 fleet: Add --filter, --no-header, --no-truncate, and --sort options
Resolves: #2503
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-01 15:35:13 +00:00
efe6fd22ce fleet: Add --fields and --json options
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-01 15:35:13 +00:00
6ee8d8a899 fleet: Use the oclif output formatter
Change-type: major
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-01 15:35:13 +00:00
c735f13636 config: Drop optional and ignored --type flag
Change-type: patch
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-01 15:35:13 +00:00
edb0fdc3c1 Drop deprecated --logs flag
Resolves: #2499
Change-type: major
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-01 15:35:13 +00:00
14a07ac7f7 Drop support for open-balena-api < v0.131.0
Resolves: #2502
Change-type: major
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-08-01 15:35:13 +00:00
264cd94be5 v13.10.0 2022-07-20 12:25:47 +00:00
2664f4e7fb Merge pull request #2504 from balena-io/add-device-view-option
Add `--view` flag to `device` command for opening a device's dashboard page
2022-07-20 12:23:37 +00:00
3ce2653881 v13.9.0 2022-07-19 12:03:18 +03:00
719860366f Merge pull request #2476 from balena-io/switch-to-compose
Switch to balena-compose
2022-07-19 08:58:59 +00:00
21ded85c7a v13.8.0 2022-07-18 22:55:10 +03:00
c91f67d27e Merge pull request #2505 from balena-io/add-note-option-for-push-and-deploy
Add `--note` option for `push` and `deploy`
2022-07-18 18:26:43 +00:00
18eedfec7f Add --note option for push and deploy
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-07-14 15:44:55 -04:00
1fe0480a8a Add --view flag to device command for opening a device's dashboard page
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-07-14 18:56:51 +00:00
c7f56d92dd Switch to balena-compose
Removes a bunch of individual dependencies by switching to `@balena/compose` which (currently) groups and manages those dependencies together in one package.

Change-type: minor
2022-07-14 13:05:21 +00:00
a92f58134f v13.7.1 2022-07-13 10:50:45 +03:00
cc6a8ef76e Merge pull request #2498 from balena-io/2462-bump-image-manager
os download: Fix resolving to draft releases
2022-07-13 07:48:41 +00:00
88f4a3d88e os download: Fix resolving to draft releases
Resolves: #2462
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-07-13 10:14:30 +03:00
f6d668684a v13.7.0 2022-07-07 11:01:22 +03:00
be7c0dc897 Merge pull request #2496 from balena-io/add-fleet-view-command
Add `--view` flag to `fleet` command for opening a fleet's dashboard page
2022-07-07 07:58:02 +00:00
566b7f97e0 Add --view flag to fleet command for opening a fleet's dashboard page
Change-type: minor
Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
2022-07-05 13:14:18 -04:00
f55dd81a19 v13.6.1 2022-06-13 21:40:58 +03:00
dba5349390 Merge pull request #2491 from balena-io/update-balena-sdk-16.22.0
Update balena-sdk to use the native OS release phase & variant fields
2022-06-13 17:25:54 +00:00
6a8dfcc664 Update balena-sdk to use the native OS release phase & variant fields
Update balena-sdk from 16.20.4 to 16.22.0

Change-type: patch
2022-06-09 17:51:55 +03:00
59e35d866f v13.6.0 2022-06-07 17:44:48 +03:00
9235c928f1 Merge pull request #2490 from balena-io/kyle/qemu-v7.0.0
Update QEMU to v7.0.0
2022-06-07 14:38:37 +00:00
3d88f0144a Update QEMU to v7.0.0
Change-type: minor
Signed-off-by: Kyle Harding <kyle@balena.io>
2022-06-06 14:56:10 -04:00
a6b461ba91 v13.5.3 2022-05-31 13:31:31 +03:00
b96da951db Merge pull request #2485 from balena-io/drop-needspasswordreset
Drop the needsPasswordReset property from the tests
2022-05-31 10:26:22 +00:00
8235cead07 Drop the needsPasswordReset property from the tests
Change-type: patch
See: https://github.com/balena-io/balena-api/pull/3665
2022-05-31 12:46:49 +03:00
30b9d9141d v13.5.2 2022-05-31 12:34:18 +03:00
03b41d9989 Merge pull request #2486 from balena-io/npm-dd
Deduplicate npm-shrinkwrap.json
2022-05-31 09:28:29 +00:00
aab3af2153 Deduplicate npm-shrinkwrap.json
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-05-31 11:46:49 +03:00
600457de61 v13.5.1 2022-05-27 02:21:54 +03:00
17db857e10 Merge pull request #2483 from balena-io/bump-preload-to-v3
Bump balena-preload to 12.1.0
2022-05-26 23:20:10 +00:00
eb45ae2a30 preload: Fix issue where balenaOS v2.98.3+ required an Internet connection to start apps
Devices with a v13+ supervisor will fail to start preloaded apps with a
v2 target state format if connectivity is not available since migration
of apps.json is not possible without API access.

This enables support for preloading v3 target state format in
images with supervisor v13 or above.

Change-type: patch
2022-05-26 20:48:07 +00:00
2eaf70bff3 v13.5.0 2022-05-25 15:01:45 +03:00
226f45f732 Merge pull request #2482 from balena-io/key-expiry
Add provisioning key expiry date option to config generate options
2022-05-25 11:59:06 +00:00
c4990f3a26 Update balena-sdk to 16.20.4
Update balena-sdk from 16.9.0 to 16.20.4

Change-type: patch
2022-05-24 21:53:12 +05:30
0195a3b18c Add provisioning key expiry date option to config generate options
Change-Type: minor
Signed-off-by: Nitish Agarwal <1592163+nitishagar@users.noreply.github.com>
2022-05-22 21:50:48 +05:30
3d90aeb122 v13.4.3 2022-05-19 21:10:42 +03:00
0571039bfe Merge pull request #2481 from balena-io/update-docker-progress
Update docker-progress to 5.1.3
2022-05-19 17:56:34 +00:00
ee668a4c5c Update docker-progress to 5.1.3
Update docker-progress from 5.0.1 to 5.1.3

Change-type: patch
2022-05-18 15:01:27 +01:00
ead4dbfab1 v13.4.2 2022-05-10 21:02:45 +03:00
0b498d09df Merge pull request #2479 from balena-io/kyle/balena-preload
preload: Fix detection of supervisor version for balenaOS v2.93.0
2022-05-10 17:08:59 +00:00
2b2c40c22d preload: Fix detection of supervisor version for balenaOS v2.93.0
Update balena-preload from 12.0.0 to 12.0.1

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2022-05-10 11:29:14 -04:00
127 changed files with 33975 additions and 6033 deletions

134
.github/actions/publish/action.yml vendored Normal file
View File

@ -0,0 +1,134 @@
---
name: package and draft GitHub release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: 'JSON stringified object containing all the inputs from the calling workflow'
required: true
secrets:
description: 'JSON stringified object containing all the secrets from the calling workflow'
required: true
variables:
description: 'JSON stringified object containing all the variables from the calling workflow'
required: true
# --- custom environment
XCODE_APP_LOADER_EMAIL:
type: string
default: 'accounts+apple@balena.io'
NODE_VERSION:
type: string
default: '16.x'
VERBOSE:
type: string
default: 'true'
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: 'composite'
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}
- name: Extract custom source artifact
shell: pwsh
working-directory: .
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Install additional tools
if: runner.os == 'Windows'
shell: bash
run: |
choco install yq
- name: Install additional tools
if: runner.os == 'macOS'
shell: bash
run: |
brew install coreutils
# https://www.electron.build/code-signing.html
# https://github.com/Apple-Actions/import-codesign-certs
- name: Import Apple code signing certificate
if: runner.os == 'macOS'
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
- name: Import Windows code signing certificate
if: runner.os == 'Windows'
shell: powershell
run: |
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
Remove-Item -path ${{ runner.temp }} -include certificate.base64
Import-PfxCertificate `
-FilePath ${{ runner.temp }}/certificate.pfx `
-CertStoreLocation Cert:\CurrentUser\My `
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
env:
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
# https://github.com/product-os/scripts/tree/master/shared
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
- name: Package release
shell: bash
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
if [[ $runner_os =~ darwin|macos|osx ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
elif [[ $runner_os =~ windows|win ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
CSC_LINK='${{ runner.temp }}\certificate.pfx'
# patches/all/oclif.patch
MSYSSHELLPATH="$(which bash)"
MSYSTEM=MSYS
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
fi
npm run package
find dist -type f -maxdepth 1
env:
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
CSC_FOR_PULL_REQUEST: true
# https://sectigo.com/resource-library/time-stamping-server
TIMESTAMP_SERVER: http://timestamp.sectigo.com
# Apple notarization (automation/build-bin.ts)
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
path: dist
retention-days: 1

59
.github/actions/test/action.yml vendored Normal file
View File

@ -0,0 +1,59 @@
---
name: test release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
variables:
description: "JSON stringified object containing all the variables from the calling workflow"
required: true
# --- custom environment
NODE_VERSION:
type: string
default: '16.x'
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Test release
shell: bash
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
if [[ -e package-lock.json ]] || [[ -e npm-shrinkwrap.json ]]; then
npm ci
else
npm i
fi
npm run build
npm run test
- name: Compress custom source
shell: pwsh
run: tar -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz
retention-days: 1

29
.github/workflows/flowzone.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Flowzone
on:
pull_request:
types: [opened, synchronize, closed]
branches: [main, master]
pull_request_target:
types: [opened, synchronize, closed]
branches: [main, master]
jobs:
flowzone:
name: Flowzone
uses: product-os/flowzone/.github/workflows/flowzone.yml@v4.7.1
# prevent duplicate workflow executions for pull_request and pull_request_target
if: |
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.event_name == 'pull_request'
) || (
github.event.pull_request.head.repo.full_name != github.repository &&
github.event_name == 'pull_request_target'
)
secrets: inherit
with:
custom_runs_on: '[["self-hosted","Linux","distro:focal","X64"],["self-hosted","Linux","distro:focal","ARM64"],["macos-12"],["windows-2019"]]'
repo_config: true
repo_description: "The official balena CLI tool."
github_prerelease: false

View File

@ -1,20 +0,0 @@
---
npm:
platforms:
- name: linux
os: ubuntu
architecture: x86_64
node_versions:
- "12"
- "14"
##
## Temporarily skip Alpine tests until the following issues are resolved:
## * https://github.com/concourse/concourse/issues/7905
## * https://github.com/product-os/balena-concourse/issues/631
##
# - name: linux
# os: alpine
# architecture: x86_64
# node_versions:
# - "12"
# - "14"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -78,8 +78,8 @@ 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 12 (min 12.8.0).**
> **Versions 13 and later are not yet fully supported.**
> **The balena CLI currently requires Node.js version 16.**
> **Versions 17 and later are not yet fully supported.**
### Install development tools
@ -89,7 +89,7 @@ some development tools to be installed first, as follows.
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 12
$ nvm install 16
```
The `curl` command line above uses
@ -106,15 +106,15 @@ recommended.
```sh
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 12
$ nvm install 16
```
#### **Windows** (not WSL)
Install:
* Node.js v12 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
* If you'd like the ability to switch between Node.js versions, install
- Node.js v16 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
instead.
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:

View File

@ -45,8 +45,6 @@ const execFileAsync = promisify(execFile);
export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version;
const arch = process.arch;
const MSYS2_BASH =
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
function dPath(...paths: string[]) {
return path.join(ROOT, 'dist', ...paths);
@ -89,7 +87,7 @@ async function diffPkgOutput(pkgOut: string) {
'tests',
'test-data',
'pkg',
`expected-warnings-${process.platform}.txt`,
`expected-warnings-${process.platform}-${arch}.txt`,
);
const absSavedPath = path.join(ROOT, relSavedPath);
const ignoreStartsWith = [
@ -182,9 +180,18 @@ async function execPkg(...args: any[]) {
* to be directly executed from inside another binary executable.)
*/
async function buildPkg() {
// https://github.com/vercel/pkg#targets
let targets = `linux-${arch}`;
// TBC: not possible to build for macOS or Windows arm64 on x64 nodes
if (process.platform === 'darwin') {
targets = `macos-x64`;
}
if (process.platform === 'win32') {
targets = `win-x64`;
}
const args = [
'--target',
'host',
'--targets',
targets,
'--output',
'build-bin/balena',
'package.json',
@ -425,20 +432,28 @@ async function renameInstallerFiles() {
/**
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
* script (which must be in the PATH) using a MSYS2 bash shell.
* executable installer using Microsoft SignTool.exe (Sign Tool)
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
*/
async function signWindowsInstaller() {
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
const exeName = renamedOclifInstallers[process.platform];
console.log(`Signing installer "${exeName}"`);
await execFileAsync(MSYS2_BASH, [
'sign-exe.sh',
// trust ...
await execFileAsync('signtool.exe', [
'sign',
'-t',
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
'-f',
exeName,
process.env.CSC_LINK,
'-p',
process.env.CSC_KEY_PASSWORD,
'-d',
`balena-cli ${version}`,
exeName,
]);
// ... but verify
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
} else {
console.log(
'Skipping installer signing step because CSC_* env vars are not set',
@ -450,14 +465,22 @@ async function signWindowsInstaller() {
* Wait for Apple Installer Notarization to continue
*/
async function notarizeMacInstaller(): Promise<void> {
const appleId = 'accounts+apple@balena.io';
const { notarize } = await import('electron-notarize');
await notarize({
appBundleId: 'io.balena.etcher',
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword: '@keychain:CLI_PASSWORD',
});
const teamId = process.env.XCODE_APP_LOADER_TEAM_ID || '66H43P8FRG';
const appleId =
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
if (appleIdPassword && teamId) {
const { notarize } = await import('@electron/notarize');
// https://github.com/electron/notarize#readme
await notarize({
tool: 'notarytool',
teamId,
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword,
});
}
}
/**

View File

@ -15,11 +15,12 @@
* limitations under the License.
*/
const stripIndent = require('common-tags/lib/stripIndent');
const _ = require('lodash');
const { promises: fs } = require('fs');
const path = require('path');
const simplegit = require('simple-git/promise');
// tslint:disable-next-line:import-blacklist
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import { promises as fs } from 'fs';
import * as path from 'path';
import { simpleGit } from 'simple-git';
const ROOT = path.normalize(path.join(__dirname, '..'));
@ -31,7 +32,7 @@ const ROOT = path.normalize(path.join(__dirname, '..'));
* using `touch`.
*/
async function checkBuildTimestamps() {
const git = simplegit(ROOT);
const git = simpleGit(ROOT);
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
const [docStat, gitStatus] = await Promise.all([
fs.stat(docFile),
@ -81,4 +82,5 @@ async function run() {
}
}
// tslint:disable-next-line:no-floating-promises
run();

View File

@ -8,19 +8,21 @@ _balena() {
local context state line curcontext="$curcontext"
# Valid top-level completions
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 )
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 api-keys app block config device device devices env fleet fleet internal key key local os release release tag util )
# Sub-completions
api_key_cmds=( generate )
api_key_cmds=( generate revoke )
app_cmds=( create )
block_cmds=( create )
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 )
device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet )
devices_cmds=( supported )
env_cmds=( add rename rm )
fleet_cmds=( create purge rename restart rm )
fleet_cmds=( create pin purge rename restart rm track-latest )
internal_cmds=( osinit )
key_cmds=( add rm )
local_cmds=( configure flash )
os_cmds=( build-config configure download initialize versions )
release_cmds=( finalize )
release_cmds=( finalize invalidate validate )
tag_cmds=( rm set )
@ -43,6 +45,12 @@ _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
;;
"block")
_describe -t block_cmds 'block_cmd' block_cmds "$@" && ret=0
;;
"config")
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
;;

View File

@ -7,19 +7,21 @@ _balena_complete()
local cur prev
# Valid top-level completions
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"
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 api-keys app block config device device devices env fleet fleet internal key key local os release release tag util"
# Sub-completions
api_key_cmds="generate"
api_key_cmds="generate revoke"
app_cmds="create"
block_cmds="create"
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"
device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet"
devices_cmds="supported"
env_cmds="add rename rm"
fleet_cmds="create purge rename restart rm"
fleet_cmds="create pin purge rename restart rm track-latest"
internal_cmds="osinit"
key_cmds="add rm"
local_cmds="configure flash"
os_cmds="build-config configure download initialize versions"
release_cmds="finalize"
release_cmds="finalize invalidate validate"
tag_cmds="rm set"
@ -37,6 +39,12 @@ _balena_complete()
api-key)
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
;;
app)
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
;;
block)
COMPREPLY=( $(compgen -W "$block_cmds" -- $cur) )
;;
config)
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
;;

View File

@ -333,11 +333,35 @@ Examples:
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## fleet &#60;fleet&#62;
Display detailed information about a single fleet.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -345,23 +369,34 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
$ balena fleet MyFleet
$ balena fleet myorg/myfleet
$ balena fleet myorg/myfleet --view
### Arguments
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
### Options
#### --view
open fleet dashboard page
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
## fleet create &#60;name&#62;
Create a new balena fleet.
@ -408,7 +443,7 @@ fleet device type (Check available types with `balena devices supported`)
Purge data from all devices belonging to a fleet.
This will clear the fleet's '/data' directory.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -416,9 +451,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -429,7 +462,7 @@ Examples:
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
### Options
@ -440,7 +473,7 @@ Rename a fleet.
Note, if the `newName` parameter is omitted, it will be
prompted for interactively.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -448,9 +481,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -462,7 +493,7 @@ Examples:
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### NEWNAME
@ -474,7 +505,7 @@ the new name for the fleet
Restart all devices belonging to a fleet.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -482,9 +513,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -495,7 +524,7 @@ Examples:
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
### Options
@ -505,7 +534,7 @@ Permanently remove a fleet.
The --yes option may be used to avoid interactive confirmation.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -513,9 +542,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -527,7 +554,7 @@ Examples:
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
### Options
@ -618,7 +645,7 @@ List all of your devices.
Devices can be filtered by fleet with the `--fleet` option.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -626,9 +653,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
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
@ -646,7 +671,7 @@ Examples:
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -j, --json
@ -680,6 +705,7 @@ Show information about a single device.
Examples:
$ balena device 7cf02a6
$ balena device 7cf02a6 --view
### Arguments
@ -689,6 +715,10 @@ the device uuid
### Options
#### --view
open device dashboard page
## device deactivate &#60;uuid&#62;
Deactivate a device.
@ -753,7 +783,7 @@ 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.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -761,9 +791,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Image configuration questions will be asked interactively unless a pre-configured
'config.json' file is provided with the '--config' option. The file can be
@ -773,13 +801,14 @@ Examples:
$ balena device init
$ balena device init -f myorg/myfleet
$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes
$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes
### Options
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -y, --yes
@ -811,6 +840,10 @@ path to the config JSON file, see `balena os build-config`
custom key name assigned to generated provisioning api key
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
## device local-mode &#60;uuid&#62;
Output current local mode status, or enable/disable local mode
@ -849,7 +882,7 @@ Move one or more devices to another fleet.
If --fleet is omitted, the fleet will be prompted for interactively.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -857,9 +890,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -878,7 +909,7 @@ comma-separated list (no blank spaces) of device UUIDs to be moved
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
## device os-update &#60;uuid&#62;
@ -892,6 +923,7 @@ Requires balenaCloud; will not work with openBalena or standalone balenaOS.
Examples:
$ balena device os-update 23c73a1
$ balena device os-update 23c73a1 --version 2.101.7
$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod
### Arguments
@ -916,9 +948,6 @@ This command will output the current public URL for the
specified device. It can also enable or disable the URL,
or output the enabled status, using the respective options.
The old command style 'balena device public-url enable <uuid>'
is deprecated, but still supported.
Examples:
$ balena device public-url 23c73a1
@ -932,10 +961,6 @@ Examples:
the uuid of the device to manage
#### LEGACYUUID
### Options
#### --enable
@ -997,7 +1022,7 @@ Register a new device with a balena fleet.
If --uuid is not provided, a new UUID will be automatically assigned.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1005,21 +1030,20 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
$ balena device register MyFleet
$ balena device register MyFleet --uuid <uuid>
$ balena device register myorg/myfleet --uuid <uuid>
$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>
### Arguments
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
### Options
@ -1027,6 +1051,10 @@ fleet name, slug (preferred), or numeric ID (deprecated)
custom uuid
#### --deviceType DEVICETYPE
device type slug (run 'balena devices supported' for possible values)
## device rename &#60;uuid&#62; [newName]
Rename a device.
@ -1233,7 +1261,7 @@ name may be null in JSON output (or 'N/A' in tabular output) if the fleet that
the device belonged to is no longer accessible by the current user (for example,
in case the current user was removed from the fleet by the fleet's owner).
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1241,9 +1269,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -1261,7 +1287,7 @@ Examples:
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -c, --config
@ -1371,7 +1397,7 @@ therefore the --service option cannot be used when the variable name starts
with a reserved prefix. When defining custom fleet variables, please avoid
these reserved prefixes.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1379,9 +1405,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -1409,7 +1433,7 @@ variable value; if omitted, use value from this process' environment
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -d, --device DEVICE
@ -1492,7 +1516,7 @@ select a service variable (may be used together with the --device option)
List all tags and their values for the specified fleet, device or release.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1500,9 +1524,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -1516,7 +1538,7 @@ Examples:
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -d, --device DEVICE
@ -1530,7 +1552,7 @@ release id
Remove a tag from a fleet, device or release.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1538,9 +1560,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -1560,7 +1580,7 @@ the key string of the tag
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -d, --device DEVICE
@ -1578,7 +1598,7 @@ You can optionally provide a value to be associated with the created
tag, as an extra argument after the tag key. If a value isn't
provided, a tag with an empty value is created.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1586,9 +1606,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -1615,7 +1633,7 @@ the optional value associated with the tag
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -d, --device DEVICE
@ -1900,7 +1918,7 @@ Examples:
#### FLEETORDEVICE
fleet name/slug/id, device uuid, or address of local device
fleet name/slug, device uuid, or address of local device
#### SERVICE
@ -1968,7 +1986,7 @@ Examples:
#### DEVICEORFLEET
device UUID or fleet name/slug/ID
device UUID or fleet name/slug
### Options
@ -2054,9 +2072,11 @@ Development images can be selected by appending `.dev` to the version.
Examples:
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.101.7
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2022.7.0
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.90.0
$ 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
@ -2140,6 +2160,9 @@ 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.
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
secure boot and disk encryption.
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
@ -2147,7 +2170,7 @@ 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
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -2155,9 +2178,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Note: This command is currently not supported on Windows natively. Windows users
are advised to install the Windows Subsystem for Linux (WSL) with Ubuntu, and use
@ -2186,7 +2207,7 @@ ask advanced configuration questions (when in interactive mode)
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### --config CONFIG
@ -2212,6 +2233,10 @@ WiFi SSID (network name) (non-interactive configuration)
Configure balenaOS to operate in development mode
#### --secureBoot
Configure balenaOS installer to opt-in secure boot and disk encryption
#### -d, --device DEVICE
device UUID
@ -2236,10 +2261,16 @@ paths to local files to place into the 'system-connections' directory
custom key name assigned to generated provisioning api key
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
## os initialize &#60;image&#62;
Initialize an os image for a device with a previously
configured operating system image.
configured operating system image and flash the
an external storage drive or the device's storage
medium depending on the device type.
Note: Initializing the device may ask for administrative permissions
@ -2290,13 +2321,16 @@ 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.
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
secure boot and disk encryption.
To configure an image for a fleet of mixed device types, use the --fleet option
alongside the --deviceType option to specify the target device type.
To avoid interactive questions, specify a command line option for each question that
would otherwise be asked.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -2304,9 +2338,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -2315,6 +2347,7 @@ Examples:
$ 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 myorg/fleet --version 2.12.7 --dev
$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot
$ 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
@ -2327,12 +2360,16 @@ a balenaOS version
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### --dev
Configure balenaOS to operate in development mode
#### --secureBoot
Configure balenaOS installer to opt-in secure boot and disk encryption
#### -d, --device DEVICE
device UUID
@ -2373,6 +2410,10 @@ supervisor cloud polling interval in minutes (e.g. for device variables)
custom key name assigned to generated provisioning api key
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
## config inject &#60;file&#62;
Inject a 'config.json' file to a balenaOS image file or attached SD card or
@ -2394,10 +2435,6 @@ the path to the config.json file to inject
### Options
#### -t, --type TYPE
ignored - no longer required
#### -d, --drive DRIVE
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
@ -2418,10 +2455,6 @@ Examples:
### Options
#### -t, --type TYPE
ignored - no longer required
#### -d, --drive DRIVE
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
@ -2449,10 +2482,6 @@ Examples:
### Options
#### -t, --type TYPE
ignored - no longer required
#### -d, --drive DRIVE
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
@ -2491,10 +2520,6 @@ the value of the config parameter to write
### Options
#### -t, --type TYPE
ignored - no longer required
#### -d, --drive DRIVE
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
@ -2515,7 +2540,7 @@ Check also the Preloading and Preregistering section of the balena CLI's advance
masterclass document:
https://www.balena.io/docs/learn/more/masterclasses/advanced-cli/#5-preloading-and-preregistering
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -2523,9 +2548,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Note that the this command requires Docker to be installed, as further detailed
in the balena CLI's installation instructions:
@ -2551,7 +2574,7 @@ the image file path
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -c, --commit COMMIT
@ -2715,6 +2738,7 @@ Examples:
$ balena push myFleet
$ balena push myFleet --source <source directory>
$ balena push myFleet -s <source directory>
$ balena push myFleet --source <source directory> --note "this is the note for this release"
$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"
$ balena push myorg/myfleet
@ -2779,7 +2803,7 @@ used (usually $HOME/.balena/secrets.yml|.json)
Don't run a live session on this push. The filesystem will not be monitored,
and changes will not be synchronized to any running containers. Note that both
this flag and --detached and required to cause the process to end once the
this flag and --detached are required to cause the process to end once the
initial build has completed.
#### -d, --detached
@ -2831,6 +2855,10 @@ by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
#### --note NOTE
The notes for this release
# Settings
## settings
@ -3010,7 +3038,7 @@ the type of device this build is for
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -e, --emulated
@ -3020,14 +3048,6 @@ Use QEMU for ARM architecture emulation during the image build
Alternative Dockerfile name/path, relative to the source folder
#### --dockercompose DOCKERCOMPOSE
Alternative docker-compose.yml name in the source root folder
#### --logs
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
#### --nologs
Hide the image build log output (produce less verbose output)
@ -3200,6 +3220,7 @@ Examples:
$ balena deploy myFleet
$ balena deploy myorg/myfleet --build --source myBuildDir/
$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"
$ balena deploy myorg/myfleet myRepo/myImage
$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"
@ -3207,7 +3228,7 @@ Examples:
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### IMAGE
@ -3240,6 +3261,10 @@ by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
#### --note NOTE
The notes for this release
#### -e, --emulated
Use QEMU for ARM architecture emulation during the image build
@ -3248,14 +3273,6 @@ Use QEMU for ARM architecture emulation during the image build
Alternative Dockerfile name/path, relative to the source folder
#### --dockercompose DOCKERCOMPOSE
Alternative docker-compose.yml name in the source root folder
#### --logs
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
#### --nologs
Hide the image build log output (produce less verbose output)
@ -3349,7 +3366,7 @@ scan the local network for balenaOS devices and prompt you to select one
from an interactive picker. This may require administrator/root privileges.
Likewise, if the fleet option is not provided then a picker will be shown.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -3357,9 +3374,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:
@ -3380,7 +3395,7 @@ the IP or hostname of device
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
fleet name or slug (preferred)
#### -i, --pollInterval POLLINTERVAL
@ -3436,7 +3451,7 @@ or hours, e.g. '12h', '2d'.
Both --device and --fleet flags accept multiple values, specified as
a comma-separated list (with no spaces).
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -3444,9 +3459,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
environments).
Examples:

View File

@ -0,0 +1,80 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
ids: string;
}
export default class RevokeCmd extends Command {
public static description = stripIndent`
Revoke balenaCloud API keys.
Revoke balenaCloud API keys with the given
comma-separated list of ids.
The given balenaCloud API keys will no longer be usable.
`;
public static examples = [
'$ balena api-key revoke 123',
'$ balena api-key revoke 123,124,456',
];
public static args = [
{
name: 'ids',
description: 'the API key ids',
required: true,
},
];
public static usage = 'api-key revoke <ids>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(RevokeCmd);
try {
const apiKeyIds = params.ids.split(',');
if (apiKeyIds.filter((apiKeyId) => !apiKeyId.match(/^\d+$/)).length > 0) {
console.log('API key ids must be positive integers');
return;
}
await Promise.all(
apiKeyIds.map(
async (id) => await getBalenaSdk().models.apiKey.revoke(Number(id)),
),
);
console.log('Successfully revoked the given API keys');
} catch (e) {
throw e;
}
}
}

View File

@ -0,0 +1,95 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
user?: void;
fleet?: string;
}
export default class ApiKeysCmd extends Command {
public static description = stripIndent`
Print a list of balenaCloud API keys.
Print a list of balenaCloud API keys.
Print a list of balenaCloud API keys for the current user or for a specific fleet with the \`--fleet\` option.
`;
public static examples = ['$ balena api-keys'];
public static args = [];
public static usage = 'api-keys';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
user: flags.boolean({
char: 'u',
description: 'show API keys for your user',
}),
fleet: cf.fleet,
};
public static authenticated = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ApiKeysCmd);
try {
const { getApplication } = await import('../../utils/sdk');
const actorId = options.fleet
? (
await getApplication(getBalenaSdk(), options.fleet, {
$select: 'actor',
})
).actor
: await getBalenaSdk().auth.getUserActorId();
const keys = await getBalenaSdk().pine.get({
resource: 'api_key',
options: {
$select: ['id', 'created_at', 'name', 'description', 'expiry_date'],
$filter: {
is_of__actor: actorId,
...(options.user
? {
name: {
$ne: null,
},
}
: {}),
},
$orderby: 'name asc',
},
});
const fields = ['id', 'name', 'created_at', 'description', 'expiry_date'];
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
keys.map((key) => _.mapValues(key, (val) => val ?? 'N/a')),
fields,
),
);
} catch (e) {
throw e;
}
}
}

150
lib/commands/app/create.ts Normal file
View File

@ -0,0 +1,150 @@
/**
* @license
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import { 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 AppCreateCmd extends Command {
public static description = stripIndent`
Create an app.
Create a new balena app.
You can specify the organization the app 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 app'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 app create MyApp',
'$ balena app create MyApp --organization mmyorg',
'$ balena app create MyApp -o myorg --type raspberry-pi',
];
public static args = [
{
name: 'name',
description: 'app name',
required: true,
},
];
public static usage = 'app create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the app should belong to',
}),
type: flags.string({
char: 't',
description:
'app device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
AppCreateCmd,
);
// 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
try {
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
applicationClass: 'app',
});
// Output
console.log(
`App created: slug "${application.slug}", device type "${deviceType}"`,
);
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: An app or block or fleet with the name "${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 apps in organization "${organization}".`,
);
}
throw err;
}
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
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

@ -0,0 +1,150 @@
/**
* @license
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import { 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 BlockCreateCmd extends Command {
public static description = stripIndent`
Create an block.
Create a new balena block.
You can specify the organization the block 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 block'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 block create MyBlock',
'$ balena block create MyBlock --organization mmyorg',
'$ balena block create MyBlock -o myorg --type raspberry-pi',
];
public static args = [
{
name: 'name',
description: 'block name',
required: true,
},
];
public static usage = 'block create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the block should belong to',
}),
type: flags.string({
char: 't',
description:
'block 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>(
BlockCreateCmd,
);
// 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
try {
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
applicationClass: 'block',
});
// Output
console.log(
`Block created: slug "${application.slug}", device type "${deviceType}"`,
);
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: An app or block or fleet with the name "${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 blocks in organization "${organization}".`,
);
}
throw err;
}
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
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

@ -20,7 +20,7 @@ import Command from '../command';
import { getBalenaSdk } from '../utils/lazy';
import * as cf from '../utils/common-flags';
import * as compose from '../utils/compose';
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
import type { ApplicationType, BalenaSDK } from 'balena-sdk';
import {
buildArgDeprecation,
dockerignoreHelp,
@ -208,7 +208,7 @@ ${dockerignoreHelp}
* buildEmulated
* buildOpts: arguments to forward to docker build command
*
* @param {DockerToolbelt} docker
* @param {Dockerode} docker
* @param {Logger} logger
* @param {ComposeOpts} composeOpts
* @param opts
@ -218,7 +218,9 @@ ${dockerignoreHelp}
logger: import('../utils/logger'),
composeOpts: ComposeOpts,
opts: {
app?: Application;
app?: {
application_type: [Pick<ApplicationType, 'supports_multicontainer'>];
};
arch: string;
deviceType: string;
buildEmulated: boolean;
@ -234,7 +236,7 @@ ${dockerignoreHelp}
opts.buildOpts.t,
);
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
const appType = opts.app?.application_type?.[0];
if (
appType != null &&
project.descriptors.length > 1 &&

View File

@ -19,13 +19,18 @@ 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, devModeInfo } from '../../utils/messages';
import type { PineDeferred } from 'balena-sdk';
import {
applicationIdInfo,
devModeInfo,
secureBootInfo,
} from '../../utils/messages';
import type { BalenaSDK, PineDeferred } from 'balena-sdk';
interface FlagsDef {
version: string; // OS version
fleet?: string;
dev?: boolean; // balenaOS development variant
secureBoot?: boolean;
device?: string;
deviceApiKey?: string;
deviceType?: string;
@ -37,6 +42,7 @@ interface FlagsDef {
wifiKey?: string;
appUpdatePollInterval?: string;
'provisioning-key-name'?: string;
'provisioning-key-expiry-date'?: string;
help: void;
}
@ -50,6 +56,8 @@ export default class ConfigGenerateCmd extends Command {
${devModeInfo.split('\n').join('\n\t\t')}
${secureBootInfo.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,6 +73,7 @@ export default class ConfigGenerateCmd extends Command {
'$ 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 myorg/fleet --version 2.12.7 --dev',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot',
'$ 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',
@ -79,9 +88,14 @@ export default class ConfigGenerateCmd extends Command {
}),
fleet: { ...cf.fleet, exclusive: ['device'] },
dev: cf.dev,
secureBoot: cf.secureBoot,
device: {
...cf.device,
exclusive: ['fleet', 'provisioning-key-name'],
exclusive: [
'fleet',
'provisioning-key-name',
'provisioning-key-expiry-date',
],
},
deviceApiKey: flags.string({
description:
@ -120,31 +134,42 @@ export default class ConfigGenerateCmd extends Command {
description: 'custom key name assigned to generated provisioning api key',
exclusive: ['device'],
}),
'provisioning-key-expiry-date': flags.string({
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
exclusive: ['device'],
}),
help: cf.help,
};
public static authenticated = true;
public async getApplication(balena: BalenaSDK, fleet: string) {
const { getApplication } = await import('../../utils/sdk');
return await getApplication(balena, fleet, {
$select: 'slug',
$expand: {
is_for__device_type: { $select: 'slug' },
},
});
}
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
await this.validateOptions(options);
let resourceDeviceType: string;
let application: ApplicationWithDeviceType | null = null;
let application: Awaited<ReturnType<typeof this.getApplication>> | null =
null;
let device:
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
| null = null;
if (options.device != null) {
const { tryAsInteger } = await import('../../utils/validation');
const rawDevice = await balena.models.device.get(
tryAsInteger(options.device),
{ $expand: { is_of__device_type: { $select: 'slug' } } },
);
const rawDevice = await balena.models.device.get(options.device, {
$expand: { is_of__device_type: { $select: 'slug' } },
});
if (!rawDevice.belongs_to__application) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(stripIndent`
@ -157,36 +182,40 @@ 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.fleet!, {
$expand: {
is_for__device_type: { $select: 'slug' },
},
})) as ApplicationWithDeviceType;
application = await this.getApplication(balena, options.fleet!);
resourceDeviceType = application.is_for__device_type[0].slug;
}
const deviceType = options.deviceType || resourceDeviceType;
const deviceManifest = await balena.models.device.getManifestBySlug(
deviceType,
);
// Check compatibility if application and deviceType provided
if (options.fleet && options.deviceType) {
const appDeviceManifest = await balena.models.device.getManifestBySlug(
resourceDeviceType,
);
const helpers = await import('../../utils/helpers');
if (
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
!(await helpers.areDeviceTypesCompatible(
resourceDeviceType,
deviceType,
))
) {
throw new balena.errors.BalenaInvalidDeviceType(
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
);
}
}
const deviceManifest =
await balena.models.config.getDeviceTypeManifestBySlug(deviceType);
const { validateSecureBootOptionAndWarn } = await import(
'../../utils/config'
);
await validateSecureBootOptionAndWarn(
options.secureBoot,
deviceType,
options.version,
);
// Prompt for values
// 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)
@ -195,7 +224,9 @@ export default class ConfigGenerateCmd extends Command {
});
answers.version = options.version;
answers.developmentMode = options.dev;
answers.secureBoot = options.secureBoot;
answers.provisioningKeyName = options['provisioning-key-name'];
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
// Generate config
const { generateDeviceConfig, generateApplicationConfig } = await import(

View File

@ -57,7 +57,6 @@ export default class ConfigInjectCmd extends Command {
public static usage = 'config inject <file>';
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};

View File

@ -47,7 +47,6 @@ export default class ConfigReadCmd extends Command {
public static usage = 'config read';
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
json: cf.json,

View File

@ -50,7 +50,6 @@ export default class ConfigReconfigureCmd extends Command {
public static usage = 'config reconfigure';
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
advanced: flags.boolean({
description: 'show advanced commands',

View File

@ -64,7 +64,6 @@ export default class ConfigWriteCmd extends Command {
public static usage = 'config write <key> <value>';
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};

View File

@ -16,7 +16,7 @@
*/
import { flags } from '@oclif/command';
import type { ImageDescriptor } from 'resin-compose-parse';
import type { ImageDescriptor } from '@balena/compose/dist/parse';
import Command from '../command';
import { ExpectedError } from '../errors';
@ -43,15 +43,13 @@ import {
parseReleaseTagKeysAndValues,
} from '../utils/compose_ts';
import { dockerCliFlags } from '../utils/docker';
import type {
Application,
ApplicationType,
DeviceType,
Release,
} from 'balena-sdk';
import type { ApplicationType, DeviceType, Release } from 'balena-sdk';
interface ApplicationWithArch extends Application {
interface ApplicationWithArch {
id: number;
arch: string;
is_for__device_type: [Pick<DeviceType, 'slug'>];
application_type: [Pick<ApplicationType, 'slug' | 'supports_multicontainer'>];
}
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
@ -60,6 +58,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
nologupload: boolean;
'release-tag'?: string[];
draft: boolean;
note?: string;
help: void;
}
@ -101,6 +100,7 @@ ${dockerignoreHelp}
public static examples = [
'$ balena deploy myFleet',
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
'$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"',
'$ balena deploy myorg/myfleet myRepo/myImage',
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
];
@ -114,7 +114,7 @@ ${dockerignoreHelp}
];
public static usage = 'deploy <fleet> [image]';
// TODO: docker-compose naming
public static flags: flags.Input<FlagsDef> = {
source: flags.string({
description:
@ -145,6 +145,7 @@ ${dockerignoreHelp}
as final by default unless this option is given.`,
default: false,
}),
note: flags.string({ description: 'The notes for this release' }),
...composeCliFlags,
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
@ -231,6 +232,9 @@ ${dockerignoreHelp}
releaseTagKeys,
releaseTagValues,
);
if (options.note) {
await sdk.models.release.setNote(release.id, options.note);
}
}
async deployProject(
@ -256,7 +260,7 @@ ${dockerignoreHelp}
'../utils/compose_ts'
);
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
const appType = opts.app.application_type[0];
try {
const project = await loadProject(
@ -313,7 +317,7 @@ ${dockerignoreHelp}
projectName: project.name,
composition: compositionToBuild,
arch: opts.app.arch,
deviceType: (opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
deviceType: opts.app.is_for__device_type[0].slug,
emulated: opts.buildEmulated,
buildOpts: opts.buildOpts,
inlineLogs: composeOpts.inlineLogs,
@ -334,7 +338,7 @@ ${dockerignoreHelp}
);
let release: Release | ComposeReleaseInfo['release'];
if (appType?.is_legacy) {
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
const { deployLegacy } = require('../utils/deploy-legacy');
const msg = getChalk().yellow(

View File

@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { ExpectedError } from '../../errors';
interface FlagsDef {
@ -43,7 +42,6 @@ export default class DeviceIdentifyCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to identify',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

View File

@ -21,7 +21,6 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import type { Application, Release } from 'balena-sdk';
@ -44,6 +43,7 @@ interface ExtendedDevice extends DeviceWithDeviceType {
interface FlagsDef {
help: void;
view: boolean;
}
interface ArgsDef {
@ -56,13 +56,15 @@ export default class DeviceCmd extends Command {
Show information about a single device.
`;
public static examples = ['$ balena device 7cf02a6'];
public static examples = [
'$ balena device 7cf02a6',
'$ balena device 7cf02a6 --view',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the device uuid',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
@ -71,13 +73,19 @@ export default class DeviceCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
view: flags.boolean({
default: false,
description: 'open device dashboard page',
}),
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceCmd,
);
const balena = getBalenaSdk();
@ -108,6 +116,14 @@ export default class DeviceCmd extends Command {
],
...expandForAppName,
})) as ExtendedDevice;
if (options.view) {
const open = await import('open');
const dashboardUrl = balena.models.device.getDashboardUrl(device.uuid);
await open(dashboardUrl, { wait: false });
return;
}
device.status = device.overall_status;
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);

View File

@ -31,6 +31,7 @@ interface FlagsDef {
config?: string;
help: void;
'provisioning-key-name'?: string;
'provisioning-key-expiry-date'?: string;
}
export default class DeviceInitCmd extends Command {
@ -69,6 +70,7 @@ export default class DeviceInitCmd extends Command {
public static examples = [
'$ balena device init',
'$ balena device init -f myorg/myfleet',
'$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes',
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes',
];
@ -97,6 +99,10 @@ export default class DeviceInitCmd extends Command {
'provisioning-key-name': flags.string({
description: 'custom key name assigned to generated provisioning api key',
}),
'provisioning-key-expiry-date': flags.string({
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
}),
help: cf.help,
};
@ -118,20 +124,16 @@ export default class DeviceInitCmd extends Command {
const balena = getBalenaSdk();
// Get application and
const application = (await getApplication(
balena,
options.fleet ||
(
await (await import('../../utils/patterns')).selectApplication()
).id,
{
$expand: {
is_for__device_type: {
$select: 'slug',
const application = options.fleet
? await getApplication(balena, options.fleet, {
$select: ['id', 'slug'],
$expand: {
is_for__device_type: {
$select: 'slug',
},
},
},
},
)) as ApplicationWithDeviceType;
})
: await (await import('../../utils/patterns')).selectApplication();
// Register new device
const deviceUuid = balena.models.device.generateUniqueKey();
@ -185,6 +187,14 @@ export default class DeviceInitCmd extends Command {
options['provisioning-key-name'],
);
}
if (options['provisioning-key-expiry-date']) {
configureCommand.push(
'--provisioning-key-expiry-date',
options['provisioning-key-expiry-date'],
);
}
await runCommand(configureCommand);
}

View File

@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
enable: boolean;
@ -52,7 +51,6 @@ export default class DeviceLocalModeCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to manage',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

View File

@ -21,6 +21,7 @@ import type {
BalenaSDK,
Device,
DeviceType,
PineOptions,
PineTypedResult,
} from 'balena-sdk';
import Command from '../../command';
@ -29,13 +30,6 @@ import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
type ExtendedDevice = PineTypedResult<
Device,
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
> & {
application_name?: string;
};
interface FlagsDef {
fleet?: string;
help: void;
@ -81,6 +75,33 @@ export default class DeviceMoveCmd extends Command {
public static authenticated = true;
private async getDevices(balena: BalenaSDK, deviceUuids: string[]) {
const deviceOptions = {
$select: 'belongs_to__application',
$expand: {
is_of__device_type: {
$select: 'is_of__cpu_architecture',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
} satisfies PineOptions<Device>;
// TODO: Refacor once `device.get()` accepts an array of uuids`
const devices = await Promise.all(
deviceUuids.map(
(uuid) =>
balena.models.device.get(uuid, deviceOptions) as Promise<
PineTypedResult<Device, typeof deviceOptions>
>,
),
);
return devices;
}
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceMoveCmd,
@ -88,43 +109,21 @@ export default class DeviceMoveCmd extends Command {
const balena = getBalenaSdk();
const { tryAsInteger } = await import('../../utils/validation');
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
// Split uuids string into array of uuids
const deviceUuids = params.uuid.split(',');
// Parse ids string into array of correct types
const deviceIds: Array<string | number> = params.uuid
.split(',')
.map((id) => tryAsInteger(id));
const devices = await this.getDevices(balena, deviceUuids);
// Get devices
const devices = await Promise.all(
deviceIds.map(
(uuid) =>
balena.models.device.get(
uuid,
expandForAppNameAndCpuArch,
) as Promise<ExtendedDevice>,
),
);
// Map application name for each device
for (const device of devices) {
const belongsToApplication = device.belongs_to__application;
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
}
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
// Disambiguate application
const { getApplication } = await import('../../utils/sdk');
// Get destination application
const application = options.fleet
? await getApplication(balena, options.fleet)
? await getApplication(balena, options.fleet, { $select: ['id', 'slug'] })
: await this.interactivelySelectApplication(balena, devices);
// Move each device
for (const uuid of deviceIds) {
for (const uuid of deviceUuids) {
try {
await balena.models.device.move(uuid, application.id);
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
@ -137,7 +136,7 @@ export default class DeviceMoveCmd extends Command {
async interactivelySelectApplication(
balena: BalenaSDK,
devices: ExtendedDevice[],
devices: Awaited<ReturnType<typeof this.getDevices>>,
) {
const { getExpandedProp } = await import('../../utils/pine');
// deduplicate the slugs
@ -156,7 +155,7 @@ export default class DeviceMoveCmd extends Command {
$select: 'slug',
},
},
} as const;
} satisfies PineOptions<DeviceType>;
const deviceTypes = (await balena.models.deviceType.getAllSupported(
deviceTypeOptions,
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
@ -183,7 +182,9 @@ export default class DeviceMoveCmd extends Command {
const application = await patterns.selectApplication(
(app) =>
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
devices.some((device) => device.application_name !== app.app_name),
devices.some(
(device) => device.belongs_to__application.__id !== app.id,
),
true,
);
return application;

View File

@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import type { Device } from 'balena-sdk';
import { ExpectedError } from '../../errors';
@ -47,6 +46,7 @@ export default class DeviceOsUpdateCmd extends Command {
`;
public static examples = [
'$ balena device os-update 23c73a1',
'$ balena device os-update 23c73a1 --version 2.101.7',
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
];
@ -54,7 +54,6 @@ export default class DeviceOsUpdateCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to update',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

103
lib/commands/device/pin.ts Normal file
View File

@ -0,0 +1,103 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
releaseToPinTo?: string;
}
export default class DevicePinCmd extends Command {
public static description = stripIndent`
Pin a device to a release.
Pin a device to a release.
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
`;
public static examples = [
'$ balena device pin 7cf02a6',
'$ balena device pin 7cf02a6 91165e5',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to pin to a release',
required: true,
},
{
name: 'releaseToPinTo',
description: 'the commit of the release for the device to get pinned to',
},
];
public static usage = 'device pin <uuid> [releaseToPinTo]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePinCmd);
const balena = getBalenaSdk();
const device = await balena.models.device.get(params.uuid, {
$expand: {
should_be_running__release: {
$select: 'commit',
},
belongs_to__application: {
$select: 'slug',
},
},
});
const pinnedRelease = getExpandedProp(
device.should_be_running__release,
'commit',
);
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
const releaseToPinTo = params.releaseToPinTo;
if (!releaseToPinTo) {
console.log(
`${
pinnedRelease
? `This device is currently pinned to ${pinnedRelease}.`
: 'This device is not currently pinned to any release.'
} \n\nTo see a list of all releases this device can be pinned to, run \`balena releases ${appSlug}\`.`,
);
} else {
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);
}
}
}

View File

@ -21,7 +21,6 @@ import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
enable: boolean;
@ -32,8 +31,6 @@ interface FlagsDef {
interface ArgsDef {
uuid: string;
// Optional hidden arg to support old command format
legacyUuid?: string;
}
export default class DevicePublicUrlCmd extends Command {
@ -43,9 +40,6 @@ export default class DevicePublicUrlCmd extends Command {
This command will output the current public URL for the
specified device. It can also enable or disable the URL,
or output the enabled status, using the respective options.
The old command style 'balena device public-url enable <uuid>'
is deprecated, but still supported.
`;
public static examples = [
@ -59,15 +53,8 @@ export default class DevicePublicUrlCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to manage',
parse: (dev) => tryAsInteger(dev),
required: true,
},
{
// Optional hidden arg to support old command format
name: 'legacyUuid',
parse: (dev) => tryAsInteger(dev),
hidden: true,
},
];
public static usage = 'device public-url <uuid>';
@ -95,25 +82,6 @@ export default class DevicePublicUrlCmd extends Command {
DevicePublicUrlCmd,
);
// Legacy command format support.
// Previously this command used the following format
// (changed due to oclif technicalities):
// `balena device public-url enable|disable|status <uuid>`
if (params.legacyUuid) {
const action = params.uuid;
if (!['enable', 'disable', 'status'].includes(action)) {
throw new ExpectedError(
`Unexpected arguments: ${params.uuid} ${params.legacyUuid}`,
);
}
options.enable = action === 'enable';
options.disable = action === 'disable';
options.status = action === 'status';
params.uuid = params.legacyUuid;
delete params.legacyUuid;
}
const balena = getBalenaSdk();
if (options.enable) {

View File

@ -63,17 +63,14 @@ export default class DevicePurgeCmd extends Command {
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
const { tryAsInteger } = await import('../../utils/validation');
const balena = getBalenaSdk();
const ux = getCliUx();
const deviceIds = params.uuid.split(',').map((id) => {
return tryAsInteger(id);
});
const deviceUuids = params.uuid.split(',');
for (const deviceId of deviceIds) {
ux.action.start(`Purging data from device ${deviceId}`);
await balena.models.device.purge(deviceId);
for (const uuid of deviceUuids) {
ux.action.start(`Purging data from device ${uuid}`);
await balena.models.device.purge(uuid);
ux.action.stop();
}
}

View File

@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
force: boolean;
@ -43,7 +42,6 @@ export default class DeviceRebootCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to reboot',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

View File

@ -25,6 +25,7 @@ import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
uuid?: string;
deviceType?: string;
help: void;
}
@ -47,6 +48,7 @@ export default class DeviceRegisterCmd extends Command {
'$ balena device register MyFleet',
'$ balena device register MyFleet --uuid <uuid>',
'$ balena device register myorg/myfleet --uuid <uuid>',
'$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>',
];
public static args: Array<IArg<any>> = [ca.fleetRequired];
@ -58,6 +60,10 @@ export default class DeviceRegisterCmd extends Command {
description: 'custom uuid',
char: 'u',
}),
deviceType: flags.string({
description:
"device type slug (run 'balena devices supported' for possible values)",
}),
help: cf.help,
};
@ -72,12 +78,18 @@ export default class DeviceRegisterCmd extends Command {
const balena = getBalenaSdk();
const application = await getApplication(balena, params.fleet);
const application = await getApplication(balena, params.fleet, {
$select: ['id', 'slug'],
});
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.slug}: ${uuid}`);
const result = await balena.models.device.register(application.id, uuid);
const result = await balena.models.device.register(
application.id,
uuid,
options.deviceType,
);
return result && result.uuid;
}

View File

@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
@ -48,7 +47,6 @@ export default class DeviceRenameCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to rename',
parse: (dev) => tryAsInteger(dev),
required: true,
},
{

View File

@ -82,24 +82,21 @@ export default class DeviceRestartCmd extends Command {
DeviceRestartCmd,
);
const { tryAsInteger } = await import('../../utils/validation');
const balena = getBalenaSdk();
const ux = getCliUx();
const deviceIds = params.uuid.split(',').map((id) => {
return tryAsInteger(id);
});
const deviceUuids = params.uuid.split(',');
const serviceNames = options.service?.split(',');
// Iterate sequentially through deviceIds.
// Iterate sequentially through deviceUuids.
// We may later want to add a batching feature,
// so that n devices are processed in parallel
for (const deviceId of deviceIds) {
ux.action.start(`Restarting services on device ${deviceId}`);
for (const uuid of deviceUuids) {
ux.action.start(`Restarting services on device ${uuid}`);
if (serviceNames) {
await this.restartServices(balena, deviceId, serviceNames);
await this.restartServices(balena, uuid, serviceNames);
} else {
await this.restartAllServices(balena, deviceId);
await this.restartAllServices(balena, uuid);
}
ux.action.stop();
}
@ -107,7 +104,7 @@ export default class DeviceRestartCmd extends Command {
async restartServices(
balena: BalenaSDK,
deviceId: number | string,
deviceUuid: string,
serviceNames: string[],
) {
const { ExpectedError, instanceOf } = await import('../../errors');
@ -116,7 +113,7 @@ export default class DeviceRestartCmd extends Command {
// Get device
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
try {
device = await balena.models.device.getWithServiceDetails(deviceId, {
device = await balena.models.device.getWithServiceDetails(deviceUuid, {
$expand: {
is_running__release: { $select: 'commit' },
},
@ -124,7 +121,7 @@ export default class DeviceRestartCmd extends Command {
} catch (e) {
const { BalenaDeviceNotFound } = await import('balena-errors');
if (instanceOf(e, BalenaDeviceNotFound)) {
throw new ExpectedError(`Device ${deviceId} not found.`);
throw new ExpectedError(`Device ${deviceUuid} not found.`);
} else {
throw e;
}
@ -136,7 +133,7 @@ export default class DeviceRestartCmd extends Command {
serviceNames.forEach((service) => {
if (!device.current_services[service]) {
throw new ExpectedError(
`Service ${service} not found on device ${deviceId}.`,
`Service ${service} not found on device ${deviceUuid}.`,
);
}
});
@ -155,7 +152,7 @@ export default class DeviceRestartCmd extends Command {
if (serviceContainer) {
restartPromises.push(
balena.models.device.restartService(
deviceId,
deviceUuid,
serviceContainer.image_id,
),
);
@ -166,32 +163,32 @@ export default class DeviceRestartCmd extends Command {
await Promise.all(restartPromises);
} catch (e) {
if (e.message.toLowerCase().includes('no online device')) {
throw new ExpectedError(`Device ${deviceId} is not online.`);
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
} else {
throw e;
}
}
}
async restartAllServices(balena: BalenaSDK, deviceId: number | string) {
async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
// Need to use device.get first to distinguish between non-existant and offline devices.
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
const { instanceOf, ExpectedError } = await import('../../errors');
try {
const device = await balena.models.device.get(deviceId);
const device = await balena.models.device.get(deviceUuid);
if (!device.is_online) {
throw new ExpectedError(`Device ${deviceId} is not online.`);
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
}
} catch (e) {
const { BalenaDeviceNotFound } = await import('balena-errors');
if (instanceOf(e, BalenaDeviceNotFound)) {
throw new ExpectedError(`Device ${deviceId} not found.`);
throw new ExpectedError(`Device ${deviceUuid} not found.`);
} else {
throw e;
}
}
await balena.models.device.restartApplication(deviceId);
await balena.models.device.restartApplication(deviceUuid);
}
}

View File

@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
yes: boolean;
@ -84,7 +83,7 @@ export default class DeviceRmCmd extends Command {
// Remove
for (const uuid of uuids) {
try {
await balena.models.device.remove(tryAsInteger(uuid));
await balena.models.device.remove(uuid);
} catch (err) {
console.info(`${err.message}, uuid: ${uuid}`);
process.exitCode = 1;

View File

@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { ExpectedError } from '../../errors';
interface FlagsDef {
@ -44,7 +43,6 @@ export default class DeviceShutdownCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to shutdown',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

View File

@ -0,0 +1,63 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceTrackFleetCmd extends Command {
public static description = stripIndent`
Make a device track the fleet's pinned release.
Make a device track the fleet's pinned release.
`;
public static examples = ['$ balena device track-fleet 7cf02a6'];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: "the uuid of the device to make track the fleet's release",
required: true,
},
];
public static usage = 'device track-fleet <uuid>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceTrackFleetCmd);
const balena = getBalenaSdk();
await balena.models.device.trackApplicationRelease(params.uuid);
}
}

View File

@ -22,13 +22,7 @@ import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
import type { Application } from 'balena-sdk';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
fleet?: string | null; // 'org/name' slug
device_type?: string | null;
}
import type { Device, PineOptions } from 'balena-sdk';
interface FlagsDef {
fleet?: string;
@ -36,6 +30,18 @@ interface FlagsDef {
json: boolean;
}
const devicesSelectFields = {
$select: [
'id',
'uuid',
'device_name',
'status',
'is_online',
'supervisor_version',
'os_version',
],
} satisfies PineOptions<Device>;
export default class DevicesCmd extends Command {
public static description = stripIndent`
List all devices.
@ -70,36 +76,39 @@ export default class DevicesCmd extends Command {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
const balena = getBalenaSdk();
const devicesOptions = {
...devicesSelectFields,
...expandForAppName,
$orderby: { device_name: 'asc' },
} satisfies PineOptions<Device>;
let devices;
const devices = (
await (async () => {
if (options.fleet != null) {
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, options.fleet, {
$select: 'slug',
$expand: {
owns__device: devicesOptions,
},
});
return application.owns__device;
}
if (options.fleet != null) {
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, options.fleet);
devices = (await balena.models.device.getAllByApplication(
application.id,
expandForAppName,
)) as ExtendedDevice[];
} else {
devices = (await balena.models.device.getAll(
expandForAppName,
)) as ExtendedDevice[];
}
return await balena.pine.get({
resource: 'device',
options: devicesOptions,
});
})()
).map((device) => ({
...device,
dashboard_url: balena.models.device.getDashboardUrl(device.uuid),
fleet: device.belongs_to__application?.[0]?.slug || null,
uuid: options.json ? device.uuid : device.uuid.slice(0, 7),
device_type: device.is_of__device_type?.[0]?.slug || null,
}));
devices = devices.map(function (device) {
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication =
device.belongs_to__application as Application[];
device.fleet = belongsToApplication?.[0]?.slug || null;
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
device.device_type = device.is_of__device_type?.[0]?.slug || null;
return device;
});
const fields = [
const fields: Array<keyof (typeof devices)[number]> = [
'id',
'uuid',
'device_name',

View File

@ -15,6 +15,7 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type * as BalenaSdk from 'balena-sdk';
import * as _ from 'lodash';
import Command from '../../command';
@ -59,36 +60,38 @@ export default class DevicesSupportedCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
const [dts, configDTs] = await Promise.all([
getBalenaSdk().models.deviceType.getAllSupported({
$expand: { is_of__cpu_architecture: { $select: 'slug' } },
$select: ['slug', 'name'],
}),
getBalenaSdk().models.config.getDeviceTypes(),
]);
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
const pineOptions = {
$select: ['slug', 'name'],
$expand: {
is_of__cpu_architecture: { $select: 'slug' },
device_type_alias: {
$select: 'is_referenced_by__alias',
$orderby: { is_referenced_by__alias: 'asc' },
},
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
const dts = (await getBalenaSdk().models.deviceType.getAllSupported(
pineOptions,
)) as Array<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>;
interface DT {
slug: string;
aliases: string[];
arch: string;
name: string;
}
let deviceTypes: DT[] = [];
for (const slug of Object.keys(dtsBySlug)) {
const configDT: Partial<typeof configDTs[0]> =
configDTsBySlug[slug] || {};
const aliases = (configDT.aliases || []).filter(
(alias) => alias !== slug,
);
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
deviceTypes.push({
slug,
let deviceTypes = dts.map((dt): DT => {
const aliases = dt.device_type_alias
.map((dta) => dta.is_referenced_by__alias)
.filter((alias) => alias !== dt.slug);
return {
slug: dt.slug,
aliases: options.json ? aliases : [aliases.join(', ')],
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
arch: dt.is_of__cpu_architecture[0]?.slug || 'n/a',
name: dt.name || 'N/A',
});
}
};
});
const fields = ['slug', 'aliases', 'arch', 'name'];
deviceTypes = _.sortBy(deviceTypes, fields);
if (options.json) {

View File

@ -151,16 +151,15 @@ export default class EnvAddCmd extends Command {
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
for (const app of options.fleet.split(',')) {
for (const appSlug of await resolveFleetSlugs(balena, options.fleet)) {
try {
await balena.models.application[varType].set(
await getFleetSlug(balena, app),
appSlug,
params.name,
params.value,
);
} catch (err) {
console.error(`${err.message}, fleet: ${app}`);
console.error(`${err.message}, fleet: ${appSlug}`);
process.exitCode = 1;
}
}
@ -181,6 +180,25 @@ export default class EnvAddCmd extends Command {
}
}
// TODO: Stop accepting application names in the next major
// and just drop this in favor of doing the .split(',') directly.
async function resolveFleetSlugs(
balena: BalenaSdk.BalenaSDK,
fleetOption: string,
) {
const fleetSlugs: string[] = [];
const { getFleetSlug } = await import('../../utils/sdk');
for (const appNameOrSlug of fleetOption.split(',')) {
try {
fleetSlugs.push(await getFleetSlug(balena, appNameOrSlug));
} catch (err) {
console.error(`${err.message}, fleet: ${appNameOrSlug}`);
process.exitCode = 1;
}
}
return fleetSlugs;
}
/**
* Add service variables for a device or fleet.
*/
@ -190,17 +208,17 @@ async function setServiceVars(
options: FlagsDef,
) {
if (options.fleet) {
for (const app of options.fleet.split(',')) {
for (const appSlug of await resolveFleetSlugs(sdk, options.fleet)) {
for (const service of options.service!.split(',')) {
try {
const serviceId = await getServiceIdForApp(sdk, app, service);
const serviceId = await getServiceIdForApp(sdk, appSlug, service);
await sdk.models.service.var.set(
serviceId,
params.name,
params.value!,
);
} catch (err) {
console.error(`${err.message}, fleet: ${app}`);
console.error(`${err.message}, fleet: ${appSlug}`);
process.exitCode = 1;
}
}
@ -245,11 +263,11 @@ async function setServiceVars(
*/
async function getServiceIdForApp(
sdk: BalenaSdk.BalenaSDK,
appName: string,
appSlug: string,
serviceName: string,
): Promise<number> {
let serviceId: number | undefined;
const services = await sdk.models.service.getAllByApplication(appName, {
const services = await sdk.models.service.getAllByApplication(appSlug, {
$filter: { service_name: serviceName },
});
if (services.length > 0) {
@ -257,7 +275,7 @@ async function getServiceIdForApp(
}
if (serviceId === undefined) {
throw new ExpectedError(
`Cannot find service ${serviceName} for fleet ${appName}`,
`Cannot find service ${serviceName} for fleet ${appSlug}`,
);
}
return serviceId;

View File

@ -16,7 +16,6 @@
*/
import { flags } from '@oclif/command';
import type { Application } from 'balena-sdk';
import Command from '../../command';
import { ExpectedError } from '../../errors';
@ -101,18 +100,22 @@ export default class FleetCreateCmd extends Command {
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
let application: Application;
try {
application = await getBalenaSdk().models.application.create({
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
});
// Output
console.log(
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
);
} 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}".`,
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
@ -123,16 +126,13 @@ export default class FleetCreateCmd extends Command {
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());
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).

View File

@ -15,19 +15,19 @@
* limitations under the License.
*/
import type { flags } from '@oclif/command';
import type { Release } from 'balena-sdk';
import type { flags as flagsType } from '@oclif/command';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { getBalenaSdk, 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;
view: boolean;
}
interface ArgsDef {
@ -45,15 +45,20 @@ export default class FleetCmd extends Command {
public static examples = [
'$ balena fleet MyFleet',
'$ balena fleet myorg/myfleet',
'$ balena fleet myorg/myfleet --view',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet <fleet>';
public static flags: flags.Input<FlagsDef> = {
public static flags: flagsType.Input<FlagsDef> = {
help: cf.help,
...(isV14() ? cf.dataOutputFlags : {}),
view: flags.boolean({
default: false,
description: 'open fleet dashboard page',
}),
...cf.dataOutputFlags,
};
public static authenticated = true;
@ -66,38 +71,34 @@ export default class FleetCmd extends Command {
const { getApplication } = await import('../../utils/sdk');
const application = (await getApplication(getBalenaSdk(), params.fleet, {
const balena = getBalenaSdk();
const application = await getApplication(balena, 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;
});
if (options.view) {
const open = await import('open');
const dashboardUrl = balena.models.application.getDashboardUrl(
application.id,
);
await open(dashboardUrl, { wait: false });
return;
}
const outputApplication = {
...application,
device_type: application.is_for__device_type[0].slug,
commit: application.should_be_running__release[0]?.commit,
};
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',
]),
);
}
await this.outputData(
outputApplication,
['app_name', 'id', 'device_type', 'slug', 'commit'],
options,
);
}
}

100
lib/commands/fleet/pin.ts Normal file
View File

@ -0,0 +1,100 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine';
interface FlagsDef {
help: void;
}
interface ArgsDef {
slug: string;
releaseToPinTo?: string;
}
export default class FleetPinCmd extends Command {
public static description = stripIndent`
Pin a fleet to a release.
Pin a fleet to a release.
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
`;
public static examples = [
'$ balena fleet pin myfleet',
'$ balena fleet pin myorg/myfleet 91165e5',
];
public static args: Array<IArg<any>> = [
{
name: 'slug',
description: 'the slug of the fleet to pin to a release',
required: true,
},
{
name: 'releaseToPinTo',
description: 'the commit of the release for the fleet to get pinned to',
},
];
public static usage = 'fleet pin <slug> [releaseToPinTo]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPinCmd);
const balena = getBalenaSdk();
const fleet = await balena.models.application.get(params.slug, {
$expand: {
should_be_running__release: {
$select: 'commit',
},
},
});
const pinnedRelease = getExpandedProp(
fleet.should_be_running__release,
'commit',
);
const releaseToPinTo = params.releaseToPinTo;
const slug = params.slug;
if (!releaseToPinTo) {
console.log(
`${
pinnedRelease
? `This fleet is currently pinned to ${pinnedRelease}.`
: 'This fleet is not currently pinned to any release.'
} \n\nTo see a list of all releases this fleet can be pinned to, run \`balena releases ${slug}\`.`,
);
} else {
await balena.models.application.pinToRelease(slug, releaseToPinTo);
}
}
}

View File

@ -65,7 +65,9 @@ export default class FleetPurgeCmd extends Command {
// balena.models.application.purge only accepts a numeric id
// so we must first fetch the app to get it's id,
const application = await getApplication(balena, params.fleet);
const application = await getApplication(balena, params.fleet, {
$select: 'id',
});
try {
await balena.models.application.purge(application.id);

View File

@ -16,7 +16,6 @@
*/
import type { flags } from '@oclif/command';
import type { ApplicationType } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
@ -78,9 +77,10 @@ export default class FleetRenameCmd extends Command {
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, params.fleet, {
$select: ['id', 'app_name', 'slug'],
$expand: {
application_type: {
$select: ['is_legacy'],
$select: 'slug',
},
},
});
@ -91,8 +91,8 @@ export default class FleetRenameCmd extends Command {
}
// Check app supports renaming
const appType = (application.application_type as ApplicationType[])?.[0];
if (appType.is_legacy) {
const appType = application.application_type[0];
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
throw new ExpectedError(
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
);
@ -133,9 +133,9 @@ export default class FleetRenameCmd extends Command {
}
// Get application again, to be sure of results
const renamedApplication = await balena.models.application.get(
application.id,
);
const renamedApplication = await getApplication(balena, application.id, {
$select: ['app_name', 'slug'],
});
// Output result
console.log(`Fleet renamed`);

View File

@ -62,9 +62,11 @@ export default class FleetRestartCmd extends Command {
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);
// Disambiguate application
const application = await getApplication(balena, params.fleet, {
$select: 'slug',
});
await balena.models.application.restart(application.id);
await balena.models.application.restart(application.slug);
}
}

View File

@ -76,9 +76,11 @@ export default class FleetRmCmd extends Command {
);
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.fleet);
const application = await getApplication(balena, params.fleet, {
$select: 'slug',
});
// Remove
await balena.models.application.remove(application.id);
await balena.models.application.remove(application.slug);
}
}

View File

@ -0,0 +1,66 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
slug: string;
}
export default class FleetTrackLatestCmd extends Command {
public static description = stripIndent`
Make this fleet track the latest release.
Make this fleet track the latest release.
`;
public static examples = [
'$ balena fleet track-latest myorg/myfleet',
'$ balena fleet track-latest myfleet',
];
public static args: Array<IArg<any>> = [
{
name: 'slug',
description: 'the slug of the fleet to make track the latest release',
required: true,
},
];
public static usage = 'fleet track-latest <slug>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetTrackLatestCmd);
const balena = getBalenaSdk();
await balena.models.application.trackLatestRelease(params.slug);
}
}

View File

@ -19,8 +19,7 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { isV14 } from '../utils/version';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import type { DataSetOutputOptions } from '../framework';
interface ExtendedApplication extends ApplicationWithDeviceType {
@ -49,7 +48,7 @@ export default class FleetsCmd extends Command {
public static usage = 'fleets';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataSetOutputFlags : {}),
...cf.dataSetOutputFlags,
help: cf.help,
};
@ -79,30 +78,17 @@ export default class FleetsCmd extends Command {
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',
]),
);
}
await this.outputData(
applications,
[
'id',
'app_name',
'slug',
'device_type',
'device_count',
'online_devices',
],
options,
);
}
}

View File

@ -88,6 +88,6 @@ export default class NoteCmd extends Command {
const balena = getBalenaSdk();
return balena.models.device.note(options.device!, params.note);
return balena.models.device.setNote(options.device, params.note);
}
}

View File

@ -46,7 +46,9 @@ export default class OrgsCmd extends Command {
const { getOwnOrganizations } = await import('../utils/sdk');
// Get organizations
const organizations = await getOwnOrganizations(getBalenaSdk());
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
// Display
console.log(

View File

@ -23,7 +23,11 @@ import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
import {
applicationIdInfo,
devModeInfo,
secureBootInfo,
} from '../../utils/messages';
const CONNECTIONS_FOLDER = '/system-connections';
@ -36,6 +40,7 @@ interface FlagsDef {
'config-wifi-key'?: string;
'config-wifi-ssid'?: string;
dev?: boolean; // balenaOS development variant
secureBoot?: boolean;
device?: string; // device UUID
'device-type'?: string;
help?: void;
@ -43,6 +48,7 @@ interface FlagsDef {
'system-connection': string[];
'initial-device-name'?: string;
'provisioning-key-name'?: string;
'provisioning-key-expiry-date'?: string;
}
interface ArgsDef {
@ -52,12 +58,14 @@ interface ArgsDef {
interface Answers {
appUpdatePollInterval: number; // in minutes
developmentMode?: boolean; // balenaOS development variant
secureBoot?: boolean;
deviceType: string; // e.g. "raspberrypi3"
network: 'ethernet' | 'wifi';
version: string; // e.g. "2.32.0+rev1"
wifiSsid?: string;
wifiKey?: string;
provisioningKeyName?: string;
provisioningKeyExpiryDate?: string;
}
export default class OsConfigureCmd extends Command {
@ -78,6 +86,8 @@ export default class OsConfigureCmd extends Command {
${devModeInfo.split('\n').join('\n\t\t')}
${secureBootInfo.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
@ -121,7 +131,7 @@ export default class OsConfigureCmd extends Command {
config: flags.string({
description:
'path to a pre-generated config.json file to be injected in the OS image',
exclusive: ['provisioning-key-name'],
exclusive: ['provisioning-key-name', 'provisioning-key-expiry-date'],
}),
'config-app-update-poll-interval': flags.integer({
description:
@ -138,7 +148,15 @@ export default class OsConfigureCmd extends Command {
description: 'WiFi SSID (network name) (non-interactive configuration)',
}),
dev: cf.dev,
device: { ...cf.device, exclusive: ['fleet', 'provisioning-key-name'] },
secureBoot: cf.secureBoot,
device: {
...cf.device,
exclusive: [
'fleet',
'provisioning-key-name',
'provisioning-key-expiry-date',
],
},
'device-type': flags.string({
description:
'device type slug (e.g. "raspberrypi3") to override the fleet device type',
@ -161,6 +179,11 @@ export default class OsConfigureCmd extends Command {
description: 'custom key name assigned to generated provisioning api key',
exclusive: ['config', 'device'],
}),
'provisioning-key-expiry-date': flags.string({
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
exclusive: ['config', 'device'],
}),
help: cf.help,
};
@ -201,7 +224,7 @@ export default class OsConfigureCmd extends Command {
is_for__device_type: { $select: 'slug' },
},
})) as ApplicationWithDeviceType;
await checkDeviceTypeCompatibility(balena, options, app);
await checkDeviceTypeCompatibility(options, app);
deviceTypeSlug =
options['device-type'] || app.is_for__device_type[0].slug;
}
@ -224,6 +247,15 @@ export default class OsConfigureCmd extends Command {
const { validateDevOptionAndWarn } = await import('../../utils/config');
await validateDevOptionAndWarn(options.dev, osVersion);
const { validateSecureBootOptionAndWarn } = await import(
'../../utils/config'
);
await validateSecureBootOptionAndWarn(
options.secureBoot,
deviceTypeSlug,
osVersion,
);
const answers: Answers = await askQuestionsForDeviceType(
deviceTypeManifest,
options,
@ -234,7 +266,9 @@ export default class OsConfigureCmd extends Command {
}
answers.version = osVersion;
answers.developmentMode = options.dev;
answers.secureBoot = options.secureBoot;
answers.provisioningKeyName = options['provisioning-key-name'];
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
if (_.isEmpty(configJson)) {
if (device) {
@ -346,17 +380,19 @@ async function getOsVersionFromImage(
* @param app Balena SDK Application model object
*/
async function checkDeviceTypeCompatibility(
sdk: BalenaSdk.BalenaSDK,
options: FlagsDef,
app: ApplicationWithDeviceType,
app: {
is_for__device_type: [Pick<BalenaSdk.DeviceType, 'slug'>];
},
) {
if (options['device-type']) {
const [appDeviceType, optionDeviceType] = await Promise.all([
sdk.models.device.getManifestBySlug(app.is_for__device_type[0].slug),
sdk.models.device.getManifestBySlug(options['device-type']),
]);
const helpers = await import('../../utils/helpers');
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
if (
!(await helpers.areDeviceTypesCompatible(
app.is_for__device_type[0].slug,
options['device-type'],
))
) {
throw new ExpectedError(
`Device type ${options['device-type']} is incompatible with fleet ${options.fleet}`,
);

View File

@ -53,9 +53,11 @@ export default class OsDownloadCmd extends Command {
`;
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.101.7',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2022.7.0',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.90.0',
'$ 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',

View File

@ -42,7 +42,9 @@ export default class OsInitializeCmd extends Command {
Initialize an os image for a device.
Initialize an os image for a device with a previously
configured operating system image.
configured operating system image and flash the
an external storage drive or the device's storage
medium depending on the device type.
${INIT_WARNING_MESSAGE}
`;

View File

@ -31,7 +31,14 @@ import { parseAsInteger } from '../utils/validation';
import { flags } from '@oclif/command';
import * as _ from 'lodash';
import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk';
import type {
Application,
BalenaSDK,
PineExpand,
PineOptions,
PineTypedResult,
Release,
} from 'balena-sdk';
import type { Preloader } from 'balena-preload';
interface FlagsDef extends DockerConnectionCliFlags {
@ -39,7 +46,7 @@ interface FlagsDef extends DockerConnectionCliFlags {
commit?: string;
'splash-image'?: string;
'dont-check-arch': boolean;
'pin-device-to-release': boolean;
'pin-device-to-release'?: boolean;
'additional-space'?: number;
'add-certificate'?: string[];
help: void;
@ -115,7 +122,7 @@ https://github.com/balena-io-examples/staged-releases\
'disable architecture compatibility check between image and fleet',
}),
'pin-device-to-release': flags.boolean({
default: false,
allowNo: true,
description:
'pin the preloaded device to the preloaded release on provision',
char: 'p',
@ -187,7 +194,7 @@ Can be repeated to add multiple certificates.\
: undefined;
const progressBars: {
[key: string]: ReturnType<typeof getVisuals>['Progress'];
[key: string]: InstanceType<ReturnType<typeof getVisuals>['Progress']>;
} = {};
const progressHandler = function (event: {
@ -201,7 +208,7 @@ Can be repeated to add multiple certificates.\
};
const spinners: {
[key: string]: ReturnType<typeof getVisuals>['Spinner'];
[key: string]: InstanceType<ReturnType<typeof getVisuals>['Spinner']>;
} = {};
const spinnerHandler = function (event: { name: string; action: string }) {
@ -223,7 +230,7 @@ Can be repeated to add multiple certificates.\
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;
const pinDevice = options['pin-device-to-release'];
if (dontCheckArch && !fleetSlug) {
throw new ExpectedError(
@ -242,7 +249,7 @@ Can be repeated to add multiple certificates.\
const dockerUtils = await import('../utils/docker');
const docker = await dockerUtils.getDocker(options);
const preloader = new balenaPreload.Preloader(
null,
undefined,
docker,
fleetSlug,
commit,
@ -250,7 +257,7 @@ Can be repeated to add multiple certificates.\
splashImage,
undefined, // TODO: Currently always undefined, investigate approach in ssh command.
dontCheckArch,
pinDevice,
pinDevice ?? false,
certificates,
additionalSpace,
);
@ -288,7 +295,7 @@ Can be repeated to add multiple certificates.\
preloader.on('error', reject);
resolve(
this.prepareAndPreload(preloader, balena, {
appId: fleetSlug,
slug: fleetSlug,
commit,
pinDevice,
}),
@ -308,7 +315,7 @@ Can be repeated to add multiple certificates.\
}
}
readonly applicationExpandOptions: PineExpand<Application> = {
readonly applicationExpandOptions = {
owns__release: {
$select: ['id', 'commit', 'end_timestamp', 'composition'],
$expand: {
@ -329,7 +336,7 @@ Can be repeated to add multiple certificates.\
should_be_running__release: {
$select: 'commit',
},
};
} satisfies PineExpand<Application>;
isCurrentCommit(commit: string) {
return commit === 'latest' || commit === 'current';
@ -343,7 +350,7 @@ Can be repeated to add multiple certificates.\
} catch {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return (await balena.models.application.getAllDirectlyAccessible({
const options = {
$select: ['id', 'slug', 'should_track_latest_release'],
$expand: this.applicationExpandOptions,
$filter: {
@ -388,11 +395,10 @@ Can be repeated to add multiple certificates.\
},
},
$orderby: 'slug asc',
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
}
>;
} satisfies PineOptions<Application>;
return (await balena.models.application.getAllDirectlyAccessible(
options,
)) as Array<PineTypedResult<Application, typeof options>>;
}
async selectApplication(deviceTypeSlug: string) {
@ -442,16 +448,16 @@ Can be repeated to add multiple certificates.\
}
async offerToDisableAutomaticUpdates(
application: Application,
application: Pick<Application, 'id' | 'should_track_latest_release'>,
commit: string,
pinDevice: boolean,
pinDevice: boolean | undefined,
) {
const balena = getBalenaSdk();
if (
this.isCurrentCommit(commit) ||
!application.should_track_latest_release ||
pinDevice
pinDevice != null
) {
return;
}
@ -470,8 +476,9 @@ through the web dashboard or programatically through the balena API / SDK.
Documentation about release policies and pinning can be found at:
https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/
Alternatively, the --pin-device-to-release flag may be used to pin only the
preloaded device to the selected release.
Alternatively, the --pin-device-to-release or --no-pin-device-to-release flags may be used
to avoid this interactive confirmation and pin only the preloaded device to the selected release
or keep it unpinned respectively.
Would you like to disable automatic updates for this fleet now?\
`;
@ -491,28 +498,28 @@ Would you like to disable automatic updates for this fleet now?\
});
}
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string) {
async getAppWithReleases(balenaSdk: BalenaSDK, slug: string) {
const { getApplication } = await import('../utils/sdk');
return (await getApplication(balenaSdk, appId, {
return await getApplication(balenaSdk, slug, {
$expand: this.applicationExpandOptions,
})) as Application & { should_be_running__release: [Release?] };
});
}
async prepareAndPreload(
preloader: Preloader,
balenaSdk: BalenaSDK,
options: {
appId?: string;
slug?: string;
commit?: string;
pinDevice: boolean;
pinDevice?: boolean;
},
) {
await preloader.prepare();
const application = options.appId
? await this.getAppWithReleases(balenaSdk, options.appId)
: await this.selectApplication(preloader.config.deviceType);
const application = options.slug
? await this.getAppWithReleases(balenaSdk, options.slug)
: await this.selectApplication(preloader.config!.deviceType);
let commit: string; // commit hash or the strings 'latest' or 'current'
@ -523,7 +530,7 @@ Would you like to disable automatic updates for this fleet now?\
if (this.isCurrentCommit(options.commit)) {
if (!appCommit) {
throw new Error(
`Unexpected empty commit hash for fleet ID "${application.id}"`,
`Unexpected empty commit hash for fleet slug "${application.slug}"`,
);
}
// handle `--commit current` (and its `--commit latest` synonym)

View File

@ -22,7 +22,7 @@ import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import type { BalenaSDK } from 'balena-sdk';
import { ExpectedError, instanceOf } from '../errors';
import { RegistrySecrets } from 'resin-multibuild';
import { RegistrySecrets } from '@balena/compose/dist/multibuild';
import { lowercaseIfSlug } from '../utils/normalization';
import {
applyReleaseTagKeysAndValues,
@ -55,6 +55,7 @@ interface FlagsDef {
'multi-dockerignore': boolean;
'release-tag'?: string[];
draft: boolean;
note?: string;
help: void;
}
@ -97,6 +98,7 @@ export default class PushCmd extends Command {
'$ balena push myFleet',
'$ balena push myFleet --source <source directory>',
'$ balena push myFleet -s <source directory>',
'$ balena push myFleet --source <source directory> --note "this is the note for this release"',
'$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"',
'$ balena push myorg/myfleet',
'',
@ -138,7 +140,6 @@ export default class PushCmd extends Command {
char: 'e',
default: false,
}),
// TODO: docker-compose naming
dockerfile: flags.string({
description:
'Alternative Dockerfile name/path, relative to the source folder',
@ -177,7 +178,7 @@ export default class PushCmd extends Command {
description: stripIndent`
Don't run a live session on this push. The filesystem will not be monitored,
and changes will not be synchronized to any running containers. Note that both
this flag and --detached and required to cause the process to end once the
this flag and --detached are required to cause the process to end once the
initial build has completed.`,
default: false,
}),
@ -242,6 +243,7 @@ export default class PushCmd extends Command {
as final by default unless this option is given.`,
default: false,
}),
note: flags.string({ description: 'The notes for this release' }),
help: cf.help,
};
@ -326,7 +328,7 @@ export default class PushCmd extends Command {
]);
const application = await getApplication(sdk, appNameOrSlug, {
$select: ['app_name', 'slug'],
$select: 'slug',
});
const opts = {
@ -355,6 +357,9 @@ export default class PushCmd extends Command {
releaseTagKeys,
releaseTagValues,
);
if (options.note) {
await sdk.models.release.setNote(releaseId, options.note);
}
} else if (releaseTagKeys.length > 0) {
throw new Error(stripIndent`
A release ID could not be parsed out of the builder's output.

View File

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

View File

@ -0,0 +1,80 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseValidateCmd extends Command {
public static description = stripIndent`
Validate a release.
Validate a release.
Valid releases are automatically deployed to devices tracking the latest
release if they are finalized.
`;
public static examples = [
'$ balena release validate a777f7345fe3d655c1c981aa642e5555',
'$ balena release validate 1234567',
];
public static usage = 'release validate <commitOrId>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to validate',
required: true,
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleaseValidateCmd);
const balena = getBalenaSdk();
const release = await balena.models.release.get(params.commitOrId, {
$select: ['id', 'is_invalidated'],
});
if (!release.is_invalidated) {
console.log(`Release ${params.commitOrId} is already validated!`);
return;
}
await balena.models.release.setIsInvalidated(release.id, false);
console.log(`Release ${params.commitOrId} validated`);
}
}

View File

@ -16,7 +16,6 @@
*/
import { flags } from '@oclif/command';
import type { LocalBalenaOsDevice } from 'balena-sync';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getCliUx, stripIndent } from '../utils/lazy';
@ -72,7 +71,7 @@ export default class ScanCmd extends Command {
public async run() {
const _ = await import('lodash');
const { discover } = await import('balena-sync');
const { discoverLocalBalenaOsDevices } = await import('../utils/discover');
const prettyjson = await import('prettyjson');
const dockerUtils = await import('../utils/docker');
@ -88,8 +87,7 @@ export default class ScanCmd extends Command {
const ux = getCliUx();
ux.action.start('Scanning for local balenaOS devices');
const localDevices: LocalBalenaOsDevice[] =
await discover.discoverLocalBalenaOsDevices(discoverTimeout);
const localDevices = await discoverLocalBalenaOsDevices(discoverTimeout);
const engineReachableDevices: boolean[] = await Promise.all(
localDevices.map(async ({ address }: { address: string }) => {
const docker = await dockerUtils.createClient({
@ -106,7 +104,7 @@ export default class ScanCmd extends Command {
}),
);
const developmentDevices: LocalBalenaOsDevice[] = localDevices.filter(
const developmentDevices = localDevices.filter(
(_localDevice, index) => engineReachableDevices[index],
);
@ -116,18 +114,15 @@ export default class ScanCmd extends Command {
_.isEqual,
);
const productionDevicesInfo = _.map(
productionDevices,
(device: LocalBalenaOsDevice) => {
return {
host: device.host,
address: device.address,
osVariant: 'production',
dockerInfo: undefined,
dockerVersion: undefined,
};
},
);
const productionDevicesInfo = productionDevices.map((device) => {
return {
host: device.host,
address: device.address,
osVariant: 'production',
dockerInfo: undefined,
dockerVersion: undefined,
};
});
// Query devices for info
const devicesInfo = await Promise.all(

View File

@ -76,8 +76,7 @@ export default class SshCmd extends Command {
public static args = [
{
name: 'fleetOrDevice',
description:
'fleet name/slug/id, device uuid, or address of local device',
description: 'fleet name/slug, device uuid, or address of local device',
required: true,
},
{

View File

@ -149,7 +149,7 @@ export default class SupportCmd extends Command {
console.log(
`Access has been granted for ${duration}, expiring ${new Date(
expiryTs,
).toLocaleString()}`,
).toISOString()}`,
);
}
}

View File

@ -90,8 +90,6 @@ export default class TagRmCmd extends Command {
throw new ExpectedError(TagRmCmd.missingResourceMessage);
}
const { tryAsInteger } = await import('../../utils/validation');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.remove(
@ -100,10 +98,7 @@ export default class TagRmCmd extends Command {
);
}
if (options.device) {
return balena.models.device.tags.remove(
tryAsInteger(options.device),
params.tagKey,
);
return balena.models.device.tags.remove(options.device, params.tagKey);
}
if (options.release) {
const { disambiguateReleaseParam } = await import(

View File

@ -105,8 +105,6 @@ export default class TagSetCmd extends Command {
params.value ??= '';
const { tryAsInteger } = await import('../../utils/validation');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.set(
@ -117,7 +115,7 @@ export default class TagSetCmd extends Command {
}
if (options.device) {
return balena.models.device.tags.set(
tryAsInteger(options.device),
options.device,
params.tagKey,
params.value,
);

View File

@ -76,8 +76,6 @@ export default class TagsCmd extends Command {
throw new ExpectedError(this.missingResourceMessage);
}
const { tryAsInteger } = await import('../utils/validation');
let tags;
if (options.fleet) {
@ -87,9 +85,7 @@ export default class TagsCmd extends Command {
);
}
if (options.device) {
tags = await balena.models.device.tags.getAllByDevice(
tryAsInteger(options.device),
);
tags = await balena.models.device.tags.getAllByDevice(options.device);
}
if (options.release) {
const { disambiguateReleaseParam } = await import(

View File

@ -82,7 +82,7 @@ export default class TunnelCmd extends Command {
public static args = [
{
name: 'deviceOrFleet',
description: 'device UUID or fleet name/slug/ID',
description: 'device UUID or fleet name/slug',
required: true,
parse: lowercaseIfSlug,
},
@ -153,19 +153,19 @@ export default class TunnelCmd extends Command {
try {
await handler(client);
logConnection(
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
client.remoteAddress ?? '',
client.remotePort ?? 0,
client.localAddress ?? '',
client.localPort ?? 0,
uuid,
remotePort,
);
} catch (err) {
logConnection(
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
client.remoteAddress ?? '',
client.remotePort ?? 0,
client.localAddress ?? '',
client.localPort ?? 0,
uuid,
remotePort,
err,

View File

@ -86,7 +86,7 @@ export class DeprecationChecker {
* @param version Semver without 'v' prefix, e.g. '12.0.0.'
*/
protected getNpmUrl(version: string) {
return `http://registry.npmjs.org/balena-cli/${version}`;
return `https://registry.npmjs.org/balena-cli/${version}`;
}
/**

View File

@ -177,7 +177,16 @@ const messages: {
Looks like the session token has expired.
Try logging in again with the "balena login" command.`,
BalenaInvalidDeviceType: (error: Error & { deviceTypeSlug?: string }) => {
BalenaInvalidDeviceType: (
error: Error & { deviceTypeSlug?: string; type?: string },
) => {
// TODO: The SDK should be throwing a different Error for this case.
if (
typeof error.type === 'string' &&
error.type.startsWith('Incompatible ')
) {
return error.type;
}
const slug = error.deviceTypeSlug ? `"${error.deviceTypeSlug}"` : 'slug';
return stripIndent`
Device type ${slug} not recognized. Perhaps misspelled?

View File

@ -24,7 +24,7 @@ import { stripIndent } from './utils/lazy';
* @param commandSignature A string like, for example:
* "push <fleetOrDevice>"
* That's literally so: "fleetOrDevice" is NOT replaced with the actual
* fleet ID or device ID. The purpose is to find out the most / least
* fleet slug or device uuid. The purpose is to find out the most / least
* used command verbs, so we can focus our development effort where it is most
* beneficial to end users.
*
@ -73,38 +73,52 @@ export async function trackCommand(commandSignature: string) {
}
}
const TIMEOUT = 4000;
/**
* Make the event tracking HTTPS request to balenaCloud's '/mixpanel' endpoint.
*/
async function sendEvent(balenaUrl: string, event: string, username?: string) {
const { default: got } = await import('got');
const trackData = {
event,
properties: {
arch: process.arch,
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
distinct_id: username,
mp_lib: 'node',
node: process.version,
platform: process.platform,
token: 'balena-main',
version: packageJSON.version,
},
};
const url = `https://api.${balenaUrl}/mixpanel/track`;
const searchParams = {
ip: 0,
verbose: 0,
data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
api_key: 'balena-main',
events: [
{
event_type: event,
user_id: username,
version_name: packageJSON.version,
event_properties: {
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
arch: process.arch,
platform: process.platform,
node: process.version,
},
},
],
};
const url = `https://data.${balenaUrl}/amplitude/2/httpapi`;
try {
await got(url, { searchParams, retry: 0, timeout: 4000 });
await got.post(url, {
json: trackData,
retry: 0,
timeout: {
// Starts when the request is initiated.
request: TIMEOUT,
// Starts when request has been flushed.
// Exits the request as soon as it's sent.
response: 0,
},
});
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] Event tracking error: ${e.message || e}`);
}
if (e instanceof got.TimeoutError) {
if (
e instanceof got.TimeoutError &&
TIMEOUT < (e.timings.phases.total ?? 0)
) {
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

View File

@ -91,7 +91,7 @@ export default class BalenaHelp extends Help {
.map((pc) => {
return commands.find((c) => c.id === pc.replace(' ', ':'));
})
.filter((c): c is typeof commands[0] => !!c);
.filter((c): c is (typeof commands)[0] => !!c);
let usageLength = 0;
for (const cmd of primaryCommands) {

View File

@ -107,16 +107,6 @@ 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:
@ -255,8 +245,8 @@ export async function getOsVersions(
);
// 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);
// unalias device type slug
slug = (await sdk.models.deviceType.get(slug, { $select: 'slug' })).slug;
if (slug !== deviceType) {
versions = await sdk.models.os.getAvailableOsVersions(slug);
}

View File

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

View File

@ -19,13 +19,12 @@ import { flags } from '@oclif/command';
import { stripIndent } from './lazy';
import { lowercaseIfSlug } from './normalization';
import { isV14 } from './version';
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
export const fleet = flags.string({
char: 'f',
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
description: 'fleet name or slug (preferred)',
parse: lowercaseIfSlug,
});
@ -75,6 +74,12 @@ export const dev: IBooleanFlag<boolean> = flags.boolean({
default: false,
});
export const secureBoot: IBooleanFlag<boolean> = flags.boolean({
description:
'Configure balenaOS installer to opt-in secure boot and disk encryption',
default: false,
});
export const drive = flags.string({
char: 'd',
description: stripIndent`
@ -97,19 +102,6 @@ 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',

View File

@ -15,8 +15,11 @@
* limitations under the License.
*/
import type { ImageModel, ReleaseModel } from 'balena-release/build/models';
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
import type {
ImageModel,
ReleaseModel,
} from '@balena/compose/dist/release/models';
import type { Composition, ImageDescriptor } from '@balena/compose/dist/parse';
import type { Pack } from 'tar-stream';
interface Image {
@ -39,7 +42,7 @@ export interface BuiltImage {
export interface TaggedImage {
localImage: import('dockerode').Image;
serviceImage: import('balena-release/build/models').ImageModel;
serviceImage: import('@balena/compose/dist/release/models').ImageModel;
serviceName: string;
logs: string;
props: BuiltImage.props;
@ -61,8 +64,6 @@ export interface ComposeOpts {
export interface ComposeCliFlags {
emulated: boolean;
dockerfile?: string;
dockercompose?: string;
logs: boolean;
nologs: boolean;
'multi-dockerignore': boolean;
'noparent-check': boolean;
@ -79,7 +80,9 @@ export interface ComposeProject {
}
export interface Release {
client: ReturnType<typeof import('balena-release').createClient>;
client: ReturnType<
typeof import('@balena/compose/dist/release').createClient
>;
release: Pick<
ReleaseModel,
| 'id'

View File

@ -19,7 +19,7 @@ import type { Renderer } from './compose_ts';
import type * as SDK from 'balena-sdk';
import type Dockerode = require('dockerode');
import * as path from 'path';
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
import type { Composition, ImageDescriptor } from '@balena/compose/dist/parse';
import type {
BuiltImage,
ComposeOpts,
@ -64,7 +64,7 @@ export function createProject(
): ComposeProject {
const yml = require('js-yaml') as typeof import('js-yaml');
const compose =
require('resin-compose-parse') as typeof import('resin-compose-parse');
require('@balena/compose/dist/parse') as typeof import('@balena/compose/dist/parse');
// both methods below may throw.
const rawComposition = yml.load(composeStr);
@ -107,7 +107,7 @@ export const createRelease = async function (
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');
require('@balena/compose/dist/release') as typeof import('@balena/compose/dist/release');
const client = releaseMod.createClient({ apiEndpoint, auth });
@ -214,7 +214,7 @@ export const getPreviousRepos = (
image: [SDK.Image];
}>;
const { getRegistryAndName } =
require('resin-multibuild') as typeof import('resin-multibuild');
require('@balena/compose/dist/multibuild') as typeof import('@balena/compose/dist/multibuild');
return Promise.all(
images.map(function (d) {
const imageName = d.image[0].is_stored_at__image_location || '';

View File

@ -16,7 +16,7 @@
*/
import { flags } from '@oclif/command';
import { BalenaSDK } from 'balena-sdk';
import type { TransposeOptions } from 'docker-qemu-transpose';
import type { TransposeOptions } from '@balena/compose/dist/emulate';
import type * as Dockerode from 'dockerode';
import { promises as fs } from 'fs';
import jsyaml = require('js-yaml');
@ -26,8 +26,8 @@ import type {
BuildConfig,
Composition,
ImageDescriptor,
} from 'resin-compose-parse';
import type * as MultiBuild from 'resin-multibuild';
} from '@balena/compose/dist/parse';
import type * as MultiBuild from '@balena/compose/dist/multibuild';
import * as semver from 'semver';
import type { Duplex, Readable } from 'stream';
import type { Pack } from 'tar-stream';
@ -118,7 +118,7 @@ export async function loadProject(
image?: string,
imageTag?: string,
): Promise<ComposeProject> {
const compose = await import('resin-compose-parse');
const compose = await import('@balena/compose/dist/parse');
const { createProject } = await import('./compose');
let composeName: string;
let composeStr: string;
@ -262,7 +262,7 @@ export async function buildProject(
opts: BuildProjectOpts,
): Promise<BuiltImage[]> {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
const compose = await import('resin-compose-parse');
const compose = await import('@balena/compose/dist/parse');
const imageDescriptors = compose.parse(opts.composition);
const renderer = await startRenderer({ imageDescriptors, ...opts });
let buildSummaryByService: Dictionary<string> | undefined;
@ -333,7 +333,7 @@ async function $buildProject(
logger.logDebug('Prepared tasks; building...');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('resin-multibuild');
const builder = await import('@balena/compose/dist/multibuild');
const builtImages = await builder.performBuilds(
tasks,
@ -481,8 +481,9 @@ async function qemuTransposeBuildStream({
throw new Error(`No buildStream for task '${task.tag}'`);
}
const transpose = await import('docker-qemu-transpose');
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
const transpose = await import('@balena/compose/dist/emulate');
const { toPosixPath } = (await import('@balena/compose/dist/multibuild'))
.PathUtils;
const transposeOptions: TransposeOptions = {
hostQemuPath: toPosixPath(binPath),
@ -508,9 +509,9 @@ async function setTaskProgressHooks({
inlineLogs?: boolean;
renderer: Renderer;
task: BuildTaskPlus;
transposeOptions?: import('docker-qemu-transpose').TransposeOptions;
transposeOptions?: import('@balena/compose/dist/emulate').TransposeOptions;
}) {
const transpose = await import('docker-qemu-transpose');
const transpose = await import('@balena/compose/dist/emulate');
// Get the service-specific log stream
const logStream = renderer.streams[task.serviceName];
task.logBuffer = [];
@ -724,16 +725,16 @@ export async function getServiceDirsFromComposition(
*
* The `image` argument may therefore refer to either a `build` or `image` property
* of a service in a docker-compose.yml file, which is a bit confusing but it matches
* the `ImageDescriptor.image` property as defined by `resin-compose-parse`.
* the `ImageDescriptor.image` property as defined by `@balena/compose/parse`.
*
* Note that `resin-compose-parse` "normalizes" the docker-compose.yml file such
* Note that `@balena/compose/parse` "normalizes" the docker-compose.yml file such
* that, if `services.service.build` is a string, it is converted to a BuildConfig
* object with the string value assigned to `services.service.build.context`:
* https://github.com/balena-io-modules/resin-compose-parse/blob/v2.1.3/src/compose.ts#L166-L167
* https://github.com/balena-io-modules/balena-compose/blob/v0.1.0/lib/parse/compose.ts#L166-L167
* This is why this implementation works when `services.service.build` is defined
* as a string in the docker-compose.yml file.
*
* @param image The `ImageDescriptor.image` attribute parsed with `resin-compose-parse`
* @param image The `ImageDescriptor.image` attribute parsed with `@balena/compose/parse`
*/
export function isBuildConfig(
image: string | BuildConfig,
@ -759,7 +760,8 @@ export async function tarDirectory(
}: TarDirectoryOptions,
): Promise<import('stream').Readable> {
const { filterFilesWithDockerignore } = await import('./ignore');
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
const { toPosixPath } = (await import('@balena/compose/dist/multibuild'))
.PathUtils;
let readFile: (file: string) => Promise<Buffer>;
if (process.platform === 'win32') {
@ -941,7 +943,7 @@ async function parseRegistrySecrets(
throw new ExpectedError('Filename must end with .json, .yml or .yaml');
}
const raw = (await fs.readFile(secretsFilename)).toString();
const multiBuild = await import('resin-multibuild');
const multiBuild = await import('@balena/compose/dist/multibuild');
const registrySecrets =
new multiBuild.RegistrySecretValidator().validateRegistrySecrets(
isYaml ? require('js-yaml').load(raw) : JSON.parse(raw),
@ -970,7 +972,7 @@ export async function makeBuildTasks(
releaseHash: string = 'unavailable',
preprocessHook?: (dockerfile: string) => string,
): Promise<MultiBuild.BuildTask[]> {
const multiBuild = await import('resin-multibuild');
const multiBuild = await import('@balena/compose/dist/multibuild');
const buildTasks = await multiBuild.splitBuildStream(composition, tarStream);
logger.logDebug('Found build tasks:');
@ -1016,7 +1018,7 @@ async function performResolution(
releaseHash: string,
preprocessHook?: (dockerfile: string) => string,
): Promise<MultiBuild.BuildTask[]> {
const multiBuild = await import('resin-multibuild');
const multiBuild = await import('@balena/compose/dist/multibuild');
const resolveListeners: MultiBuild.ResolveListeners = {};
const resolvePromise = new Promise<never>((_resolve, reject) => {
resolveListeners.error = [reject];
@ -1081,7 +1083,7 @@ async function validateSpecifiedDockerfile(
dockerfilePath: string,
): Promise<string> {
const { contains, toNativePath, toPosixPath } = (
await import('resin-multibuild')
await import('@balena/compose/dist/multibuild')
).PathUtils;
const nativeProjectPath = path.normalize(projectPath);
@ -1241,7 +1243,7 @@ async function pushAndUpdateServiceImages(
token: string,
images: TaggedImage[],
afterEach: (
serviceImage: import('balena-release/build/models').ImageModel,
serviceImage: import('@balena/compose/dist/release/models').ImageModel,
props: object,
) => void,
) {
@ -1326,12 +1328,14 @@ async function pushAndUpdateServiceImages(
async function pushServiceImages(
docker: Dockerode,
logger: Logger,
pineClient: ReturnType<typeof import('balena-release').createClient>,
pineClient: ReturnType<
typeof import('@balena/compose/dist/release').createClient
>,
taggedImages: TaggedImage[],
token: string,
skipLogUpload: boolean,
): Promise<void> {
const releaseMod = await import('balena-release');
const releaseMod = await import('@balena/compose/dist/release');
logger.logInfo('Pushing images to registry...');
await pushAndUpdateServiceImages(
docker,
@ -1361,8 +1365,8 @@ export async function deployProject(
skipLogUpload: boolean,
projectPath: string,
isDraft: boolean,
): Promise<import('balena-release/build/models').ReleaseModel> {
const releaseMod = await import('balena-release');
): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
const releaseMod = await import('@balena/compose/dist/release');
const { createRelease, tagServiceImages } = await import('./compose');
const tty = (await import('./tty'))(process.stdout);
@ -1639,7 +1643,6 @@ function truncateString(str: string, len: number): string {
return str.slice(0, str.lastIndexOf('\n'));
}
// TODO: docker-compose naming
export const composeCliFlags: flags.Input<ComposeCliFlags> = {
emulated: flags.boolean({
description:
@ -1650,14 +1653,6 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
description:
'Alternative Dockerfile name/path, relative to the source folder',
}),
dockercompose: flags.string({
description:
'Alternative docker-compose.yml name in the source root folder',
}),
logs: flags.boolean({
description:
'No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.',
}),
nologs: flags.boolean({
description:
'Hide the image build log output (produce less verbose output)',

View File

@ -52,10 +52,14 @@ export interface ImgConfig {
os?: {
sshKeys?: string[];
};
installer?: {
secureboot?: boolean;
};
}
export async function generateApplicationConfig(
application: BalenaSdk.Application,
application: Pick<BalenaSdk.Application, 'slug'>,
options: {
version: string;
appUpdatePollInterval?: number;
@ -63,6 +67,7 @@ export async function generateApplicationConfig(
os?: {
sshKeys?: string[];
};
secureBoot?: boolean;
},
): Promise<ImgConfig> {
options = {
@ -84,6 +89,12 @@ export async function generateApplicationConfig(
: options.os.sshKeys;
}
// configure installer secure boot opt-in if specified
if (options.secureBoot) {
config.installer ??= {};
config.installer.secureboot = options.secureBoot;
}
return config;
}
@ -165,3 +176,54 @@ export async function validateDevOptionAndWarn(
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.`);
}
/**
* Chech whether the `--secureBoot` option of commands related to OS configuration
* such as `os configure` and `config generate` is compatible with a given
* OS release, and print a warning regarding the consequences of using that
* option.
*/
export async function validateSecureBootOptionAndWarn(
secureBoot?: boolean,
slug?: string,
version?: string,
logger?: import('./logger'),
) {
if (!secureBoot) {
return;
}
const { ExpectedError } = await import('../errors');
if (!version) {
throw new ExpectedError(`Error: No version provided`);
}
if (!slug) {
throw new ExpectedError(`Error: No device type provided`);
}
const sdk = getBalenaSdk();
const [osRelease] = await sdk.models.os.getAllOsVersions(slug, {
$select: 'contract',
$filter: { raw_version: `${version.replace(/^v/, '')}` },
});
if (!osRelease) {
throw new ExpectedError(`Error: No ${version} release for ${slug}`);
}
const contract = osRelease.contract ? JSON.parse(osRelease.contract) : null;
if (
contract?.provides.some((entry: Dictionary<string>) => {
return entry.type === 'sw.feature' && entry.slug === 'secureboot';
})
) {
if (!logger) {
const Logger = await import('./logger');
logger = Logger.getLogger();
}
logger.logInfo(stripIndent`
The '--secureBoot' option is being used to configure a balenaOS installer image
into secure boot and full disk encryption.`);
} else {
throw new ExpectedError(
`Error: The '--secureBoot' option is not supported for ${slug} in ${version}`,
);
}
}

View File

@ -18,13 +18,13 @@
import * as semver from 'balena-semver';
import * as Docker from 'dockerode';
import * as _ from 'lodash';
import { Composition } from 'resin-compose-parse';
import { Composition } from '@balena/compose/dist/parse';
import {
BuildTask,
getAuthConfigObj,
LocalImage,
RegistrySecrets,
} from 'resin-multibuild';
} from '@balena/compose/dist/multibuild';
import type { Readable } from 'stream';
import { BALENA_ENGINE_TMP_PATH } from '../../config';
@ -44,7 +44,7 @@ import { displayBuildLog } from './logs';
import { stripIndent } from '../lazy';
const LOCAL_APPNAME = 'localapp';
const LOCAL_RELEASEHASH = 'localrelease';
const LOCAL_RELEASEHASH = '10ca12e1ea5e';
const LOCAL_PROJECT_NAME = 'local_image';
// Define the logger here so the debug output
@ -209,9 +209,9 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
globalLogger.logDebug('Fetching device information...');
const deviceInfo = await api.getDeviceInformation();
let buildLogs: Dictionary<string> | undefined;
let imageIds: Dictionary<string[]> | undefined;
if (!opts.nolive) {
buildLogs = {};
imageIds = {};
}
const { awaitInterruptibleTask } = await import('../helpers');
@ -223,7 +223,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
deviceInfo,
globalLogger,
opts,
buildLogs,
imageIds,
);
globalLogger.outputDeferredMessages();
@ -265,7 +265,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
docker,
logger: globalLogger,
composition: project.composition,
buildLogs: buildLogs!,
imageIds: imageIds!,
deployOpts: opts,
});
promises.push(livepush.init());
@ -312,6 +312,14 @@ function connectToDocker(host: string, port: number): Docker {
});
}
function extractDockerArrowMessage(outputLine: string): string | undefined {
const arrowTest = /^.*\s*-+>\s*(.+)/i;
const match = arrowTest.exec(outputLine);
if (match != null) {
return match[1];
}
}
async function performBuilds(
composition: Composition,
tarStream: Readable,
@ -319,9 +327,9 @@ async function performBuilds(
deviceInfo: DeviceInfo,
logger: Logger,
opts: DeviceDeployOptions,
buildLogs?: Dictionary<string>,
imageIds?: Dictionary<string[]>,
): Promise<BuildTask[]> {
const multibuild = await import('resin-multibuild');
const multibuild = await import('@balena/compose/dist/multibuild');
const buildTasks = await makeBuildTasks(
composition,
@ -345,14 +353,29 @@ async function performBuilds(
// If we're passed a build logs object make sure to set it
// up properly
let logHandlers: ((serviceName: string, line: string) => void) | undefined;
if (buildLogs != null) {
const lastArrowMessage: Dictionary<string> = {};
if (imageIds != null) {
for (const task of buildTasks) {
if (!task.external) {
buildLogs[task.serviceName] = '';
imageIds[task.serviceName] = [];
}
}
logHandlers = (serviceName: string, line: string) => {
buildLogs[serviceName] += `${line}\n`;
// If this was a from line, take the last found
// image id and save it
if (
/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
lastArrowMessage[serviceName] != null
) {
imageIds[serviceName].push(lastArrowMessage[serviceName]);
} else {
const msg = extractDockerArrowMessage(line);
if (msg != null) {
lastArrowMessage[serviceName] = msg;
}
}
};
}
@ -370,7 +393,7 @@ async function performBuilds(
const imagesToRemove: string[] = [];
// Now tag any external images with the correct name that they should be,
// as this won't be done by resin-multibuild
// as this won't be done by @balena/compose/multibuild
await Promise.all(
localImages.map(async (localImage) => {
if (localImage.external) {
@ -413,12 +436,26 @@ export async function rebuildSingleTask(
// the logs, so any calller who wants to keep track of
// this should provide the following callback
containerIdCb?: (id: string) => void,
): Promise<string> {
const multibuild = await import('resin-multibuild');
): Promise<string[]> {
const multibuild = await import('@balena/compose/dist/multibuild');
// First we run the build task, to get the new image id
let buildLogs = '';
const stageIds = [] as string[];
let lastArrowMessage: string | undefined;
const logHandler = (_s: string, line: string) => {
buildLogs += `${line}\n`;
// If this was a FROM line, take the last found
// image id and save it as a stage id
if (
/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
lastArrowMessage != null
) {
stageIds.push(lastArrowMessage);
} else {
const msg = extractDockerArrowMessage(line);
if (msg != null) {
lastArrowMessage = msg;
}
}
if (containerIdCb != null) {
const match = line.match(/^\s*--->\s*Running\s*in\s*([a-f0-9]*)\s*$/i);
@ -477,7 +514,7 @@ export async function rebuildSingleTask(
]);
}
return buildLogs;
return stageIds;
}
function assignOutputHandlers(
@ -533,10 +570,17 @@ async function assignDockerBuildOpts(
await Promise.all(
buildTasks.map(async (task: BuildTask) => {
task.dockerOpts = {
cachefrom: images,
labels: {
'io.resin.local.image': '1',
'io.resin.local.service': task.serviceName,
...(task.dockerOpts || {}),
...{
cachefrom: images,
labels: {
'io.resin.local.image': '1',
'io.resin.local.service': task.serviceName,
},
t: getImageNameFromTask(task),
nocache: opts.nocache,
forcerm: true,
pull: opts.pull,
},
t: getImageNameFromTask(task),
nocache: opts.nocache,
@ -544,7 +588,7 @@ async function assignDockerBuildOpts(
pull: opts.pull,
};
if (task.external) {
task.dockerOpts.authconfig = await getAuthConfigObj(
task.dockerOpts.authconfig = getAuthConfigObj(
task.imageName!,
opts.registrySecrets,
);

View File

@ -21,8 +21,8 @@ import * as fs from 'fs';
import Livepush, { ContainerNotRunningError } from 'livepush';
import * as _ from 'lodash';
import * as path from 'path';
import type { Composition } from 'resin-compose-parse';
import type { BuildTask } from 'resin-multibuild';
import type { Composition } from '@balena/compose/dist/parse';
import type { BuildTask } from '@balena/compose/dist/multibuild';
import { instanceOf } from '../../errors';
import Logger = require('../logger');
@ -52,7 +52,6 @@ interface MonitoredContainer {
containerId: string;
}
type BuildLogs = Dictionary<string>;
type StageImageIDs = Dictionary<string[]>;
export interface LivepushOpts {
@ -62,7 +61,7 @@ export interface LivepushOpts {
docker: Dockerode;
api: DeviceAPI;
logger: Logger;
buildLogs: BuildLogs;
imageIds: StageImageIDs;
deployOpts: DeviceDeployOptions;
}
@ -97,7 +96,7 @@ export class LivepushManager {
this.api = opts.api;
this.logger = opts.logger;
this.deployOpts = opts.deployOpts;
this.imageIds = LivepushManager.getMultistageImageIDs(opts.buildLogs);
this.imageIds = opts.imageIds;
}
public async init(): Promise<void> {
@ -250,7 +249,7 @@ export class LivepushManager {
cwd: serviceContext,
followSymlinks: true,
ignoreInitial: true,
ignored: (filePath: string, stats: fs.Stats | undefined) => {
ignored: (filePath: string, stats?: fs.Stats) => {
if (!stats) {
try {
// sync because chokidar defines a sync interface
@ -297,33 +296,6 @@ export class LivepushManager {
return new Dockerfile(content).generateLiveDockerfile();
}
private static getMultistageImageIDs(buildLogs: BuildLogs): StageImageIDs {
const stageIds: StageImageIDs = {};
_.each(buildLogs, (log, serviceName) => {
stageIds[serviceName] = [];
const lines = log.split(/\r?\n/);
let lastArrowMessage: string | undefined;
for (const line of lines) {
// If this was a from line, take the last found
// image id and save it
if (
/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
lastArrowMessage != null
) {
stageIds[serviceName].push(lastArrowMessage);
} else {
const msg = LivepushManager.extractDockerArrowMessage(line);
if (msg != null) {
lastArrowMessage = msg;
}
}
}
});
return stageIds;
}
private async awaitDeviceStateSettle(): Promise<void> {
// Cache the state to avoid unnecessary calls
this.lastDeviceStatus = await this.api.getStatus();
@ -405,9 +377,9 @@ export class LivepushManager {
);
}
let buildLog: string;
let stageImages: string[];
try {
buildLog = await rebuildSingleTask(
stageImages = await rebuildSingleTask(
serviceName,
this.docker,
this.logger,
@ -466,17 +438,13 @@ export class LivepushManager {
);
}
const buildLogs: Dictionary<string> = {};
buildLogs[serviceName] = buildLog;
const stageImages = LivepushManager.getMultistageImageIDs(buildLogs);
const dockerfile = new Dockerfile(buildTask.dockerfile!);
instance.livepush = await Livepush.init({
dockerfile,
context: buildTask.context!,
containerId: container.containerId,
stageImages: stageImages[serviceName],
stageImages,
docker: this.docker,
});
this.assignLivepushOutputHandlers(serviceName, instance.livepush);
@ -536,16 +504,6 @@ export class LivepushManager {
});
}
private static extractDockerArrowMessage(
outputLine: string,
): string | undefined {
const arrowTest = /^.*\s*-+>\s*(.+)/i;
const match = arrowTest.exec(outputLine);
if (match != null) {
return match[1];
}
}
private getDockerfilePathFromTask(task: BuildTask): string[] {
switch (task.projectType) {
case 'Standard Dockerfile':

View File

@ -155,12 +155,8 @@ export function displayLogObject<T extends Log>(
system: boolean,
filterServices?: string[],
): void {
let toPrint: string;
if (obj.timestamp != null) {
toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`;
} else {
toPrint = `[${new Date().toLocaleString()}]`;
}
const d = obj.timestamp != null ? new Date(obj.timestamp) : new Date();
let toPrint = `[${d.toISOString()}]`;
if (obj.serviceName != null) {
if (filterServices) {

40
lib/utils/discover.ts Normal file
View File

@ -0,0 +1,40 @@
import { enumerateServices, findServices } from 'resin-discoverable-services';
interface LocalBalenaOsDevice {
address: string;
host: string;
osVariant?: string;
port: number;
}
// Although we only check for 'balena-ssh', we know, implicitly, that balenaOS
// devices come with 'rsync' installed that can be used over SSH.
const avahiBalenaSshTag = 'resin-ssh';
export async function discoverLocalBalenaOsDevices(
timeout = 4000,
): Promise<LocalBalenaOsDevice[]> {
const availableServices = await enumerateServices();
const serviceDefinitions = Array.from(availableServices)
.filter((s) => Array.from(s.tags).includes(avahiBalenaSshTag))
.map((s) => s.service);
if (serviceDefinitions.length === 0) {
throw new Error(
`Could not find any available '${avahiBalenaSshTag}' services`,
);
}
const services = await findServices(serviceDefinitions, timeout);
return services.map(function (service) {
// User referer address to get device IP. This will work fine assuming that
// a device only advertises own services.
const {
referer: { address },
host,
port,
} = service;
return { address, host, port };
});
}

View File

@ -105,7 +105,7 @@ export interface BuildOpts {
cachefrom?: string[];
nocache?: boolean;
pull?: boolean;
registryconfig?: import('resin-multibuild').RegistrySecrets;
registryconfig?: import('@balena/compose/dist/multibuild').RegistrySecrets;
squash?: boolean;
t?: string; // only the tag portion of the image name, e.g. 'abc' in 'myimg:abc'
}
@ -132,7 +132,7 @@ export function generateBuildOpts(options: {
'cache-from'?: string;
nocache: boolean;
pull?: boolean;
'registry-secrets'?: import('resin-multibuild').RegistrySecrets;
'registry-secrets'?: import('@balena/compose/dist/multibuild').RegistrySecrets;
squash: boolean;
tag?: string;
}): BuildOpts {

View File

@ -107,21 +107,53 @@ export async function getManifest(
deviceType: string,
): Promise<BalenaSdk.DeviceTypeJson.DeviceType> {
const init = await import('balena-device-init');
const sdk = getBalenaSdk();
const manifest = await init.getImageManifest(image);
if (manifest != null) {
return manifest;
if (
manifest != null &&
manifest.slug !== deviceType &&
manifest.slug !== (await sdk.models.deviceType.get(deviceType)).slug
) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
`The device type of the provided OS image ${manifest.slug}, does not match the expected device type ${deviceType}`,
);
}
return getBalenaSdk().models.device.getManifestBySlug(deviceType);
return (
manifest ??
(await sdk.models.config.getDeviceTypeManifestBySlug(deviceType))
);
}
export const areDeviceTypesCompatible = (
appDeviceType: BalenaSdk.DeviceTypeJson.DeviceType,
osDeviceType: BalenaSdk.DeviceTypeJson.DeviceType,
) =>
getBalenaSdk().models.os.isArchitectureCompatibleWith(
osDeviceType.arch,
appDeviceType.arch,
) && !!appDeviceType.isDependent === !!osDeviceType.isDependent;
export const areDeviceTypesCompatible = async (
appDeviceTypeSlug: string,
osDeviceTypeSlug: string,
) => {
if (appDeviceTypeSlug === osDeviceTypeSlug) {
return true;
}
const sdk = getBalenaSdk();
const pineOptions = {
$select: 'is_of__cpu_architecture',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
const [appDeviceType, osDeviceType] = await Promise.all(
[appDeviceTypeSlug, osDeviceTypeSlug].map(
(dtSlug) =>
sdk.models.deviceType.get(dtSlug, pineOptions) as Promise<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>,
),
);
return sdk.models.os.isArchitectureCompatibleWith(
osDeviceType.is_of__cpu_architecture[0].slug,
appDeviceType.is_of__cpu_architecture[0].slug,
);
};
export async function osProgressHandler(step: InitializeEmitter) {
step.on('stdout', process.stdout.write.bind(process.stdout));
@ -148,14 +180,13 @@ export async function osProgressHandler(step: InitializeEmitter) {
});
}
export async function getAppWithArch(
applicationName: string,
): Promise<ApplicationWithDeviceType & { arch: string }> {
export async function getAppWithArch(applicationName: string) {
const { getApplication } = await import('./sdk');
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
const balena = getBalenaSdk();
const app = await getApplication(balena, applicationName, {
$expand: {
application_type: {
$select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'],
$select: ['name', 'slug', 'supports_multicontainer'],
},
is_for__device_type: {
$select: 'slug',
@ -166,20 +197,10 @@ export async function getAppWithArch(
},
},
},
};
const balena = getBalenaSdk();
const app = (await getApplication(
balena,
applicationName,
options,
)) as ApplicationWithDeviceType;
const { getExpanded } = await import('./pine');
});
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
arch: app.is_for__device_type[0].is_of__cpu_architecture[0].slug,
};
}
@ -410,25 +431,11 @@ export function getProxyConfig(): ProxyConfig | undefined {
export const expandForAppName = {
$expand: {
belongs_to__application: { $select: ['app_name', 'slug'] as any },
belongs_to__application: { $select: ['app_name', 'slug'] },
is_of__device_type: { $select: 'slug' },
is_running__release: { $select: 'commit' },
},
} as const;
export const expandForAppNameAndCpuArch = {
$expand: {
...expandForAppName.$expand,
is_of__device_type: {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
} as const;
} satisfies BalenaSdk.PineOptions<BalenaSdk.Device>;
/**
* Use the `readline` library on Windows to install SIGINT handlers.

View File

@ -92,7 +92,6 @@ async function readDockerIgnoreFile(projectDir: string): Promise<string> {
return dockerIgnoreStr;
}
// TODO: docker-compose naming
/**
* Create an instance of '@balena/dockerignore', initialized with the contents
* of a .dockerignore file (if any) found at the given directory argument, plus

View File

@ -86,7 +86,6 @@ 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.`;
// TODO: docker-compose naming
export const dockerignoreHelp =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
@ -138,7 +137,7 @@ adding exception patterns to the applicable .dockerignore file(s), for example
- https://www.npmjs.com/package/@balena/dockerignore`;
export const applicationIdInfo = `\
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
Fleets may be specified by fleet name or slug. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the \`balena fleets\` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -146,9 +145,7 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public 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). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.`;
environments).`;
export const applicationNameNote = `\
Fleets may be specified by fleet name or slug. Slugs are recommended because
@ -172,6 +169,10 @@ 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 secureBootInfo = `\
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
secure boot and disk encryption.`;
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

View File

@ -14,8 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { Application, BalenaSDK, Device, Organization } from 'balena-sdk';
import _ = require('lodash');
import type {
Application,
BalenaSDK,
Device,
Organization,
PineOptions,
PineTypedResult,
} from 'balena-sdk';
import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
@ -108,22 +114,22 @@ export function askLoginType() {
});
}
export function selectDeviceType() {
return getBalenaSdk()
.models.config.getDeviceTypes()
.then((deviceTypes) => {
deviceTypes = _.sortBy(deviceTypes, 'name').filter(
(dt) => dt.state !== 'DISCONTINUED',
);
return getCliForm().ask({
message: 'Device Type',
type: 'list',
choices: _.map(deviceTypes, ({ slug: value, name }) => ({
name,
value,
})),
});
});
export async function selectDeviceType() {
const sdk = getBalenaSdk();
let deviceTypes = await sdk.models.deviceType.getAllSupported();
if (deviceTypes.length === 0) {
// Without this open-balena users would get an empty list
// until we add a hostApps import in open-balena.
deviceTypes = await sdk.models.deviceType.getAll();
}
return getCliForm().ask({
message: 'Device Type',
type: 'list',
choices: deviceTypes.map(({ slug: value, name }) => ({
name,
value,
})),
});
}
/**
@ -157,46 +163,57 @@ export async function confirm(
}
}
export function selectApplication(
filter?: (app: ApplicationWithDeviceType) => boolean,
const selectApplicationPineOptions = {
$select: ['id', 'slug', 'app_name'],
$expand: {
is_for__device_type: {
$select: 'slug',
},
},
} satisfies PineOptions<Application>;
type SelectApplicationResult = PineTypedResult<
Application,
typeof selectApplicationPineOptions
>;
export async function selectApplication(
filter?: (app: SelectApplicationResult) => boolean,
errorOnEmptySelection = false,
) {
const balena = getBalenaSdk();
return balena.models.application
.hasAny()
.then(async (hasAnyApplications) => {
if (!hasAnyApplications) {
throw new ExpectedError('No fleets found');
}
let apps = (await balena.models.application.getAllDirectlyAccessible(
selectApplicationPineOptions,
)) as SelectApplicationResult[];
const apps = (await balena.models.application.getAllDirectlyAccessible({
$expand: {
is_for__device_type: {
$select: 'slug',
},
},
})) as ApplicationWithDeviceType[];
return apps.filter(filter || _.constant(true));
})
.then((applications) => {
if (errorOnEmptySelection && applications.length === 0) {
throw new ExpectedError('No suitable fleets found for selection');
}
return getCliForm().ask({
message: 'Select an application',
type: 'list',
choices: _.map(applications, (application) => ({
name: `${application.app_name} (${application.slug}) [${application.is_for__device_type[0].slug}]`,
value: application,
})),
});
});
if (!apps.length) {
throw new ExpectedError('No fleets found');
}
if (filter != null) {
apps = apps.filter(filter);
}
if (errorOnEmptySelection && apps.length === 0) {
throw new ExpectedError('No suitable fleets found for selection');
}
return getCliForm().ask({
message: 'Select an application',
type: 'list',
choices: apps.map((application) => ({
name: `${application.app_name} (${application.slug}) [${application.is_for__device_type[0].slug}]`,
value: application,
})),
});
}
export async function selectOrganization(organizations?: Organization[]) {
export async function selectOrganization(
organizations?: Array<Pick<Organization, 'handle' | 'name'>>,
) {
// Use either provided orgs (if e.g. already loaded) or load from cloud
organizations =
organizations || (await getBalenaSdk().models.organization.getAll());
organizations ??= await getBalenaSdk().models.organization.getAll({
$select: ['name', 'handle'],
});
return getCliForm().ask({
message: 'Select an organization',
type: 'list',
@ -252,116 +269,88 @@ export async function awaitDeviceOsUpdate(
return uuid;
}
export function inferOrSelectDevice(preferredUuid: string) {
const balena = getBalenaSdk();
return balena.models.device.getAll().then((devices) => {
const onlineDevices = devices.filter((device) => device.is_online);
if (_.isEmpty(onlineDevices)) {
throw new ExpectedError("You don't have any devices online");
}
const defaultUuid = _(onlineDevices).map('uuid').includes(preferredUuid)
? preferredUuid
: onlineDevices[0].uuid;
return getCliForm().ask({
message: 'Select a device',
type: 'list',
default: defaultUuid,
choices: _.map(onlineDevices, (device) => ({
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(
0,
7,
)})`,
value: device.uuid,
})),
});
});
}
/*
* Given applicationOrDevice, which may be
* - an application name
* - an application slug
* - an application id (integer)
* Given fleetOrDevice, which may be
* - a fleet name
* - a fleet slug
* - a device uuid
* Either:
* - in case of device uuid, return uuid of device after verifying that it exists and is online.
* - in case of application, return uuid of device user selects from list of online devices.
*
* TODO: Modify this when app IDs dropped.
* - in case of fleet, return uuid of device user selects from list of online devices.
*/
export async function getOnlineTargetDeviceUuid(
sdk: BalenaSDK,
applicationOrDevice: string,
fleetOrDevice: string,
) {
const logger = (await import('../utils/logger')).getLogger();
// If looks like UUID, probably device
if (validation.validateUuid(applicationOrDevice)) {
if (validation.validateUuid(fleetOrDevice)) {
let device: Device;
try {
logger.logDebug(
`Trying to fetch device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
`Trying to fetch device by UUID ${fleetOrDevice} (${typeof fleetOrDevice})`,
);
device = await sdk.models.device.get(applicationOrDevice, {
device = await sdk.models.device.get(fleetOrDevice, {
$select: ['uuid', 'is_online'],
});
if (!device.is_online) {
throw new ExpectedError(
`Device with UUID ${applicationOrDevice} is offline`,
);
throw new ExpectedError(`Device with UUID ${fleetOrDevice} is offline`);
}
return device.uuid;
} catch (err) {
const { BalenaDeviceNotFound } = await import('balena-errors');
if (instanceOf(err, BalenaDeviceNotFound)) {
logger.logDebug(`Device with UUID ${applicationOrDevice} not found`);
// Now try app
logger.logDebug(`Device with UUID ${fleetOrDevice} not found`);
// Now try application
} else {
throw err;
}
}
}
// Not a device UUID, try app
let app: Application;
try {
logger.logDebug(`Fetching fleet ${applicationOrDevice}`);
const { getApplication } = await import('./sdk');
app = await getApplication(sdk, applicationOrDevice);
} catch (err) {
const { BalenaApplicationNotFound } = await import('balena-errors');
if (instanceOf(err, BalenaApplicationNotFound)) {
throw new ExpectedError(
`Fleet or Device not found: ${applicationOrDevice}`,
);
} else {
throw err;
// Not a device UUID, try application
const application = await (async () => {
try {
logger.logDebug(`Fetching fleet ${fleetOrDevice}`);
const { getApplication } = await import('./sdk');
return await getApplication(sdk, fleetOrDevice, {
$select: ['id', 'slug'],
$expand: {
owns__device: {
$select: ['device_name', 'uuid'],
$filter: { is_online: true },
},
},
});
} catch (err) {
const { BalenaApplicationNotFound } = await import('balena-errors');
if (instanceOf(err, BalenaApplicationNotFound)) {
throw new ExpectedError(`Fleet or Device not found: ${fleetOrDevice}`);
} else {
throw err;
}
}
}
})();
// App found, load its devices
const devices = await sdk.models.device.getAllByApplication(app.id, {
$select: ['device_name', 'uuid'],
$filter: { is_online: true },
});
const devices = application.owns__device;
// Throw if no devices online
if (_.isEmpty(devices)) {
if (!devices.length) {
throw new ExpectedError(
`Fleet ${app.slug} found, but has no devices online.`,
`Fleet ${application.slug} found, but has no devices online.`,
);
}
// Ask user to select from online devices for application
// Ask user to select from online devices for fleet
return getCliForm().ask({
message: `Select a device on fleet ${app.slug}`,
message: `Select a device on fleet ${application.slug}`,
type: 'list',
default: devices[0].uuid,
choices: _.map(devices, (device) => ({
choices: devices.map((device) => ({
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(0, 7)})`,
value: device.uuid,
})),
@ -375,7 +364,7 @@ export function selectFromList<T>(
return getCliForm().ask<T>({
message,
type: 'list',
choices: _.map(choices, (s) => ({
choices: choices.map((s) => ({
name: s.name,
value: s,
})),

View File

@ -16,10 +16,10 @@ limitations under the License.
import type { OptionalNavigationResource } from 'balena-sdk';
export const getExpanded = <T>(obj: OptionalNavigationResource<T>) =>
export const getExpanded = <T extends {}>(obj: OptionalNavigationResource<T>) =>
(Array.isArray(obj) && obj[0]) || undefined;
export const getExpandedProp = <T, K extends keyof T>(
export const getExpandedProp = <T extends {}, K extends keyof T>(
obj: OptionalNavigationResource<T>,
key: K,
) => (Array.isArray(obj) && obj[0] && obj[0][key]) || undefined;

View File

@ -163,12 +163,61 @@ async function getOsVersion(deviceIp: string): Promise<string> {
return match[1];
}
const dockerPort = 2375;
const dockerTimeout = 2000;
async function selectLocalBalenaOsDevice(timeout = 4000): Promise<string> {
const { discoverLocalBalenaOsDevices } = await import('../utils/discover');
const { SpinnerPromise } = getVisuals();
const devices = await new SpinnerPromise({
promise: discoverLocalBalenaOsDevices(timeout),
startMessage: 'Discovering local balenaOS devices..',
stopMessage: 'Reporting discovered devices',
});
const responsiveDevices: typeof devices = [];
const Docker = await import('dockerode');
await Promise.all(
devices.map(async function (device) {
const address = device?.address;
if (!address) {
return;
}
try {
const docker = new Docker({
host: address,
port: dockerPort,
timeout: dockerTimeout,
});
await docker.ping();
responsiveDevices.push(device);
} catch {
return;
}
}),
);
if (!responsiveDevices.length) {
throw new Error('Could not find any local balenaOS devices');
}
return getCliForm().ask({
message: 'select a device',
type: 'list',
default: devices[0].address,
choices: responsiveDevices.map((device) => ({
name: `${device.host || 'untitled'} (${device.address})`,
value: device.address,
})),
});
}
async function selectLocalDevice(): Promise<string> {
const { forms } = await import('balena-sync');
let hostnameOrIp;
try {
hostnameOrIp = await forms.selectLocalBalenaOsDevice();
const hostnameOrIp = await selectLocalBalenaOsDevice();
console.error(`==> Selected device: ${hostnameOrIp}`);
return hostnameOrIp;
} catch (e) {
if (e.message.toLowerCase().includes('could not find any')) {
throw new ExpectedError(e);
@ -176,8 +225,6 @@ async function selectLocalDevice(): Promise<string> {
throw e;
}
}
return hostnameOrIp;
}
async function selectAppFromList(
@ -198,31 +245,37 @@ async function selectAppFromList(
async function getOrSelectApplication(
sdk: BalenaSdk.BalenaSDK,
deviceType: string,
deviceTypeSlug: string,
appName?: string,
): Promise<ApplicationWithDeviceType> {
const _ = await import('lodash');
const pineOptions = {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
const [deviceType, allDeviceTypes] = await Promise.all([
sdk.models.deviceType.get(deviceTypeSlug, pineOptions) as Promise<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>,
sdk.models.deviceType.getAllSupported(pineOptions) as Promise<
Array<BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>>
>,
]);
const allDeviceTypes = await sdk.models.config.getDeviceTypes();
const deviceTypeManifest = _.find(allDeviceTypes, { slug: deviceType });
if (!deviceTypeManifest) {
throw new ExpectedError(`"${deviceType}" is not a valid device type`);
}
const compatibleDeviceTypes = _(allDeviceTypes)
.filter(
(dt) =>
sdk.models.os.isArchitectureCompatibleWith(
deviceTypeManifest.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceTypeManifest.isDependent &&
dt.state !== 'DISCONTINUED',
const compatibleDeviceTypes = allDeviceTypes
.filter((dt) =>
sdk.models.os.isArchitectureCompatibleWith(
deviceType.is_of__cpu_architecture[0].slug,
dt.is_of__cpu_architecture[0].slug,
),
)
.map((type) => type.slug)
.value();
.map((type) => type.slug);
if (!appName) {
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceType);
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceTypeSlug);
}
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
@ -257,13 +310,13 @@ async function getOrSelectApplication(
undefined,
true,
);
return await createApplication(sdk, deviceType, name);
return await createApplication(sdk, deviceTypeSlug, name);
}
// We've found at least one fleet with the given name.
// Filter out fleets for non-matching device types and see what we're left with.
const validApplications = applications.filter((app) =>
_.includes(compatibleDeviceTypes, app.is_for__device_type[0].slug),
compatibleDeviceTypes.includes(app.is_for__device_type[0].slug),
);
if (validApplications.length === 0) {
@ -334,13 +387,7 @@ async function createApplication(
try {
await sdk.models.application.getDirectlyAccessible(appName, {
$filter: {
$or: [
{ slug: { $startswith: `${username!.toLowerCase()}/` } },
// TODO: do we still need the following filter? Is it for
// old openBalena instances where slugs were equal to the
// app name and did not contain the slash character?
{ $not: { slug: { $contains: '/' } } },
],
slug: { $startswith: `${username!.toLowerCase()}/` },
},
});
// TODO: This is the only example in the codebase where `printErrorMessage()`
@ -380,7 +427,7 @@ async function generateApplicationConfig(
) {
const { generateApplicationConfig: configGen } = await import('./config');
const manifest = await sdk.models.device.getManifestBySlug(
const manifest = await sdk.models.config.getDeviceTypeManifestBySlug(
app.is_for__device_type[0].slug,
);
const opts =

View File

@ -21,7 +21,7 @@ import { ExpectedError } from '../errors';
import { getBalenaSdk, stripIndent } from './lazy';
import Logger = require('./logger');
export const QEMU_VERSION = 'v6.0.0+balena1';
export const QEMU_VERSION = 'v7.0.0+balena1';
export const QEMU_BIN_NAME = 'qemu-execve';
export function qemuPathInContext(context: string) {

View File

@ -17,7 +17,7 @@ import type { BalenaSDK } from 'balena-sdk';
import * as JSONStream from 'JSONStream';
import * as readline from 'readline';
import * as request from 'request';
import { RegistrySecrets } from 'resin-multibuild';
import { RegistrySecrets } from '@balena/compose/dist/multibuild';
import type * as Stream from 'stream';
import streamToPromise = require('stream-to-promise');
import type { Pack } from 'tar-stream';

View File

@ -20,11 +20,21 @@ import type {
BalenaSDK,
Organization,
PineOptions,
PineTypedResult,
} from 'balena-sdk';
export async function getApplication(
sdk: BalenaSDK,
nameOrSlugOrId: string | number,
): Promise<Application>;
export async function getApplication<TP extends PineOptions<Application>>(
sdk: BalenaSDK,
nameOrSlugOrId: string | number,
options?: TP,
): Promise<PineTypedResult<Application, TP>>;
/**
* Get a fleet object, disambiguating the fleet identifier which may be a
* a fleet slug, name or numeric database ID (as a string).
* a fleet slug or name.
* TODO: add support for fleet UUIDs.
*/
export async function getApplication(
@ -32,46 +42,27 @@ export async function getApplication(
nameOrSlugOrId: string | number,
options?: PineOptions<Application>,
): Promise<Application> {
const { looksLikeFleetSlug, looksLikeInteger } = await import('./validation');
const { looksLikeFleetSlug } = await import('./validation');
if (
typeof nameOrSlugOrId === 'string' &&
looksLikeFleetSlug(nameOrSlugOrId)
!looksLikeFleetSlug(nameOrSlugOrId)
) {
return await sdk.models.application.getDirectlyAccessible(
// Not a slug: must be an app name.
// TODO: revisit this logic when we add support for fleet UUIDs.
return await sdk.models.application.getAppByName(
nameOrSlugOrId,
options,
'directly_accessible',
);
}
if (typeof nameOrSlugOrId === 'number' || looksLikeInteger(nameOrSlugOrId)) {
try {
// Test for existence of app with this numerical ID
return await sdk.models.application.getDirectlyAccessible(
Number(nameOrSlugOrId),
options,
);
} catch (e) {
if (typeof nameOrSlugOrId === 'number') {
throw e;
}
const { instanceOf } = await import('../errors');
const { BalenaApplicationNotFound } = await import('balena-errors');
if (!instanceOf(e, BalenaApplicationNotFound)) {
throw e;
}
// App with this numerical ID not found, but there may be an app with this numerical name.
}
}
// Not a slug and not a numeric database ID: must be an app name.
// TODO: revisit this logic when we add support for fleet UUIDs.
return await sdk.models.application.getAppByName(
return await sdk.models.application.getDirectlyAccessible(
nameOrSlugOrId,
options,
'directly_accessible',
);
}
/**
* Given a fleet name, slug or numeric database ID, return its slug.
* Given a fleet name or slug, return its slug.
* This function conditionally makes an async SDK/API call to retrieve the
* application object, which can be wasteful if the application object is
* required before or after the call to this function. If this is the case,
@ -79,38 +70,50 @@ export async function getApplication(
*/
export async function getFleetSlug(
sdk: BalenaSDK,
nameOrSlugOrId: string | number,
nameOrSlug: string,
): Promise<string> {
const { looksLikeFleetSlug } = await import('./validation');
if (
typeof nameOrSlugOrId === 'string' &&
looksLikeFleetSlug(nameOrSlugOrId)
) {
return nameOrSlugOrId.toLowerCase();
if (!looksLikeFleetSlug(nameOrSlug)) {
// Not a slug: must be an app name.
// TODO: revisit this logic when we add support for fleet UUIDs.
return (await getApplication(sdk, nameOrSlug, { $select: 'slug' })).slug;
}
return (await getApplication(sdk, nameOrSlugOrId)).slug;
return nameOrSlug.toLowerCase();
}
export async function getOwnOrganizations(
sdk: BalenaSDK,
): Promise<Organization[]>;
export async function getOwnOrganizations<TP extends PineOptions<Organization>>(
sdk: BalenaSDK,
options: TP,
): Promise<Array<PineTypedResult<Organization, TP>>>;
/**
* Wraps the sdk organization.getAll method,
* restricting to those orgs user is a member of
*/
export async function getOwnOrganizations(
sdk: BalenaSDK,
options?: PineOptions<Organization>,
): Promise<Organization[]> {
return await sdk.models.organization.getAll({
$filter: {
organization_membership: {
$any: {
$alias: 'orm',
$expr: {
orm: {
user: await sdk.auth.getUserId(),
return await sdk.models.organization.getAll(
sdk.utils.mergePineOptions(
{
$filter: {
organization_membership: {
$any: {
$alias: 'orm',
$expr: {
orm: {
user: await sdk.auth.getUserId(),
},
},
},
},
},
$orderby: 'name asc',
},
},
$orderby: 'name asc',
});
options,
),
);
}

View File

@ -151,23 +151,23 @@ export async function runRemoteCommand({
let exitCode: number | undefined;
let exitSignal: NodeJS.Signals | undefined;
try {
[exitCode, exitSignal] = await new Promise<[number, NodeJS.Signals]>(
(resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', (code, signal) => resolve([code, signal]));
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', (code, signal) =>
resolve([code ?? undefined, signal ?? undefined]),
);
if (ps.stdin && stdin && typeof stdin !== 'string') {
stdin.pipe(ps.stdin);
}
if (ps.stdout && stdout && typeof stdout !== 'string') {
ps.stdout.pipe(stdout);
}
if (ps.stderr && stderr && typeof stderr !== 'string') {
ps.stderr.pipe(stderr);
}
},
);
if (ps.stdin && stdin && typeof stdin !== 'string') {
stdin.pipe(ps.stdin);
}
if (ps.stdout && stdout && typeof stdout !== 'string') {
ps.stdout.pipe(stdout);
}
if (ps.stderr && stderr && typeof stderr !== 'string') {
ps.stderr.pipe(stderr);
}
});
} catch (error) {
const msg = [
`ssh failed with exit code=${exitCode} signal=${exitSignal}:`,

View File

@ -19,8 +19,10 @@ import * as UpdateNotifier from 'update-notifier';
import packageJSON = require('../../package.json');
// Check for an update once a day. 1 day granularity should be
// enough, rather than every run.
// Check for an update at most once a day. 1 day granularity should be
// enough, rather than every run. Note because we show the information
// from the *last* time we ran, if the cli has not been run for a while
// the update info can be out of date.
const balenaUpdateInterval = 1000 * 60 * 60 * 24 * 1;
let notifier: UpdateNotifier.UpdateNotifier;

View File

@ -21,13 +21,3 @@ import { version } from '../../package.json';
export function isVersionGTE(v: string): boolean {
return semver.gte(process.env.BALENA_CLI_VERSION_OVERRIDE || version, v);
}
let v14: boolean;
/** Feature switch for the next major version of the CLI */
export function isV14(): boolean {
if (v14 === undefined) {
v14 = isVersionGTE('14.0.0');
}
return v14;
}

29622
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "13.4.1",
"version": "16.7.0",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -27,9 +27,8 @@
"scripts": [
"build/**/*.js",
"node_modules/balena-sdk/es2018/index.js",
"node_modules/balena-sync/build/**/*.js",
"node_modules/pinejs-client-request/node_modules/pinejs-client-core/es2018/index.js",
"node_modules/resin-compose-parse/build/schemas/*.json"
"node_modules/@balena/compose/dist/parse/schemas/*.json"
],
"assets": [
"build/auth/pages/*.ejs",
@ -90,12 +89,11 @@
"author": "Balena Inc. (https://balena.io/)",
"license": "Apache-2.0",
"engines": {
"node": ">=12.8.0 <13.0.0",
"npm": "<7.0.0"
"node": ">=16 <18"
},
"husky": {
"hooks": {
"pre-commit": "node automation/check-npm-version.js && node automation/check-doc.js"
"pre-commit": "node automation/check-npm-version.js && ts-node automation/check-doc.ts"
}
},
"oclif": {
@ -115,7 +113,8 @@
]
},
"devDependencies": {
"@balena/lint": "^6.2.0",
"@balena/lint": "^6.2.2",
"@electron/notarize": "^2.0.0",
"@oclif/config": "^1.18.2",
"@oclif/parser": "^3.8.6",
"@octokit/plugin-throttling": "^3.5.1",
@ -127,13 +126,14 @@
"@types/chai-as-promised": "^7.1.4",
"@types/cli-truncate": "^2.0.0",
"@types/common-tags": "^1.8.1",
"@types/dockerode": "^3.3.8",
"@types/dockerode": "^3.3.9",
"@types/ejs": "^3.1.0",
"@types/express": "^4.17.13",
"@types/fs-extra": "^9.0.13",
"@types/global-agent": "^2.1.1",
"@types/global-tunnel-ng": "^2.1.1",
"@types/http-proxy": "^1.17.8",
"@types/inquirer": "^7.3.3",
"@types/intercept-stdout": "^0.1.0",
"@types/is-root": "^2.1.2",
"@types/js-yaml": "^4.0.5",
@ -147,7 +147,7 @@
"@types/ndjson": "^2.0.1",
"@types/net-keepalive": "^0.4.1",
"@types/nock": "^11.1.0",
"@types/node": "^12.20.42",
"@types/node": "^16.18.25",
"@types/node-cleanup": "^2.1.2",
"@types/parse-link-header": "^1.0.1",
"@types/prettyjson": "^0.0.30",
@ -171,7 +171,6 @@
"cross-env": "^7.0.3",
"deep-object-diff": "^1.1.0",
"diff": "^5.0.0",
"electron-notarize": "^1.0.0",
"ent": "^2.2.0",
"filehound": "^1.17.5",
"fs-extra": "^9.1.0",
@ -184,16 +183,17 @@
"mocha": "^8.4.0",
"mock-require": "^3.0.3",
"nock": "^13.2.1",
"parse-link-header": "^1.0.1",
"pkg": "^5.5.1",
"parse-link-header": "^2.0.0",
"pkg": "^5.8.1",
"publish-release": "^1.6.1",
"rewire": "^5.0.0",
"simple-git": "^2.48.0",
"simple-git": "^3.14.1",
"sinon": "^11.1.2",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
"typescript": "^5.1.3"
},
"dependencies": {
"@balena/compose": "^2.3.0",
"@balena/dockerignore": "^1.0.2",
"@balena/es-version": "^1.0.1",
"@oclif/command": "^1.8.16",
@ -201,19 +201,16 @@
"@sentry/node": "^6.16.1",
"@types/fast-levenshtein": "0.0.1",
"@types/update-notifier": "^4.1.1",
"JSONStream": "^1.0.3",
"balena-config-json": "^4.2.0",
"balena-device-init": "^6.0.0",
"balena-errors": "^4.7.1",
"balena-errors": "^4.7.3",
"balena-image-fs": "^7.0.6",
"balena-image-manager": "^7.1.1",
"balena-preload": "^12.0.0",
"balena-release": "^3.2.0",
"balena-sdk": "^16.9.0",
"balena-image-manager": "^9.0.0",
"balena-preload": "^14.0.0",
"balena-sdk": "^17.0.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.7",
"balena-settings-client": "^5.0.2",
"balena-settings-storage": "^7.0.0",
"balena-sync": "^11.0.2",
"bluebird": "^3.7.2",
"body-parser": "^1.19.1",
"chalk": "^3.0.0",
@ -225,11 +222,10 @@
"common-tags": "^1.7.2",
"denymount": "^2.3.0",
"docker-modem": "3.0.0",
"docker-progress": "^5.0.1",
"docker-qemu-transpose": "^1.1.1",
"dockerode": "^3.3.1",
"docker-progress": "^5.1.3",
"dockerode": "3.3.3",
"ejs": "^3.1.6",
"etcher-sdk": "^6.2.1",
"etcher-sdk": "^8.5.3",
"event-stream": "3.3.4",
"express": "^4.17.2",
"fast-boot2": "^1.1.0",
@ -245,6 +241,7 @@
"is-elevated": "^3.0.0",
"is-root": "^2.1.0",
"js-yaml": "^4.1.0",
"JSONStream": "^1.0.3",
"klaw": "^3.0.0",
"livepush": "^3.5.1",
"lodash": "^4.17.21",
@ -263,10 +260,9 @@
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
"request": "^2.88.2",
"resin-cli-form": "^2.0.2",
"resin-cli-visuals": "^1.8.0",
"resin-compose-parse": "^2.1.3",
"resin-cli-visuals": "^1.8.3",
"resin-discoverable-services": "^2.0.4",
"resin-doodles": "^0.2.0",
"resin-multibuild": "^4.12.2",
"resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
@ -288,6 +284,6 @@
"windosu": "^0.3.0"
},
"versionist": {
"publishedAt": "2022-04-11T16:10:47.566Z"
"publishedAt": "2023-07-17T19:59:50.432Z"
}
}

View File

@ -1,24 +1,24 @@
diff --git a/node_modules/@oclif/parser/lib/errors.js b/node_modules/@oclif/parser/lib/errors.js
index 0c93a81..95d06be 100644
index 39936e3..23e3925 100644
--- a/node_modules/@oclif/parser/lib/errors.js
+++ b/node_modules/@oclif/parser/lib/errors.js
@@ -13,7 +13,8 @@ const m = deps_1.default()
@@ -14,7 +14,8 @@ const m = deps_1.default()
.add('list', () => require('./list'));
class CLIParseError extends errors_1.CLIError {
constructor(options) {
- options.message += '\nSee more help with --help';
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
+ options.message += `\nSee more help with ${help}`;
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
+ options.message += `\nSee more help with ${help}`;
super(options.message);
this.parse = options.parse;
}
@@ -34,22 +35,24 @@ class InvalidArgsSpecError extends CLIParseError {
@@ -35,22 +36,24 @@ class InvalidArgsSpecError extends CLIParseError {
exports.InvalidArgsSpecError = InvalidArgsSpecError;
class RequiredArgsError extends CLIParseError {
constructor({ args, parse }) {
- let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}`;
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
const namedArgs = args.filter(a => a.name);
if (namedArgs.length > 0) {
const list = m.list.renderList(namedArgs.map(a => [a.name, a.description]));
@ -32,7 +32,7 @@ index 0c93a81..95d06be 100644
exports.RequiredArgsError = RequiredArgsError;
class RequiredFlagError extends CLIParseError {
constructor({ flag, parse }) {
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
const usage = m.list.renderList(m.help.flagUsages([flag], { displayRequired: false }));
const message = `Missing required flag:\n${usage}`;
- super({ parse, message });
@ -41,15 +41,15 @@ index 0c93a81..95d06be 100644
}
}
diff --git a/node_modules/@oclif/parser/lib/list.js b/node_modules/@oclif/parser/lib/list.js
index 3907cc0..b689ca1 100644
index 9d020b7..6ea9eb9 100644
--- a/node_modules/@oclif/parser/lib/list.js
+++ b/node_modules/@oclif/parser/lib/list.js
@@ -21,7 +21,7 @@ function renderList(items) {
@@ -22,7 +22,7 @@ function renderList(items) {
}
left = left.padEnd(maxLength);
right = linewrap(maxLength + 2, right);
- return `${left} ${right}`;
+ return `${left} : ${right}`;
+ return `${left} : ${right}`;
});
return lines.join('\n');
}

View File

@ -1,15 +1,16 @@
diff --git a/node_modules/open/index.js b/node_modules/open/index.js
index 3bf5373..e042b64 100644
index 13147d0..ff161dd 100644
--- a/node_modules/open/index.js
+++ b/node_modules/open/index.js
@@ -11,7 +11,9 @@ const pAccess = promisify(fs.access);
const pExecFile = promisify(childProcess.execFile);
@@ -10,7 +10,10 @@ const pAccess = promisify(fs.access);
const pReadFile = promisify(fs.readFile);
// Path to included `xdg-open`.
-const localXdgOpenPath = path.join(__dirname, 'xdg-open');
+const localXdgOpenPath = process.pkg
+ ? path.join(path.dirname(process.execPath), 'xdg-open')
+ : path.join(__dirname, 'xdg-open');
+
// Convert a path from WSL format to Windows format:
// `/mnt/c/Program Files/Example/MyApp.exe` → `C:\Program Files\Example\MyApp.exe`
/**
Get the mount point for fixed drives in WSL.

View File

@ -1,8 +1,8 @@
diff --git a/node_modules/node-gyp-build/index.js b/node_modules/node-gyp-build/index.js
index b5096ed..7cd451a 100644
--- a/node_modules/node-gyp-build/index.js
+++ b/node_modules/node-gyp-build/index.js
@@ -29,6 +29,9 @@ load.path = function (dir) {
diff --git a/node_modules/node-gyp-build/node-gyp-build.js b/node_modules/node-gyp-build/node-gyp-build.js
index 61b398e..3cc3be8 100644
--- a/node_modules/node-gyp-build/node-gyp-build.js
+++ b/node_modules/node-gyp-build/node-gyp-build.js
@@ -30,6 +30,9 @@ load.resolve = load.path = function (dir) {
if (process.env[name + '_PREBUILD']) dir = process.env[name + '_PREBUILD']
} catch (err) {}

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