Compare commits

...

55 Commits

Author SHA1 Message Date
b616fbdd79 v21.1.5 2025-04-03 13:28:12 +00:00
81edfbbae1 Merge pull request #2930 from balena-io/bump_dockerode_cachefrom
Update dockerode/docker-modem dependencies for cachefrom fix
2025-04-03 09:27:11 -04:00
663e83c3b8 Dedupe dependencies 2025-04-02 18:50:31 -04:00
b650f8ff6d Update dockerode/docker-modem dependencies for fixes
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-04-02 18:47:43 -04:00
58234f17e1 v21.1.4 2025-04-02 09:16:30 +00:00
77905f4a74 Merge pull request #2928 from balena-io/comment_sig_file
Add comment with example of signature file in a flasher image
2025-04-02 09:15:23 +00:00
30076fabe6 Dedupe dependencies 2025-04-01 18:25:20 -04:00
28703bb5ae Add comment with secure boot signature file example for preload
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-04-01 18:25:16 -04:00
37b3c6abe9 v21.1.3 2025-03-28 16:57:03 +00:00
b4e473e4d4 Merge pull request #2927 from balena-io/fix-ob-device-detail
Fix device detail for open balena
2025-03-28 16:55:56 +00:00
0d4e411777 Fix device detail for open balena
Change-type: patch
2025-03-28 13:29:52 -03:00
7e6f2189e8 v21.1.2 2025-03-27 12:20:26 +00:00
3903daf8a8 Merge pull request #2914 from balena-io/deny-preload-sb
Deny preload for an image with secure boot enabled
2025-03-27 08:19:14 -04:00
18bc0d61e7 Dedupe dependencies 2025-03-26 22:55:54 -04:00
7f2daeebb0 Deny preload for an image with secure boot enabled
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-03-26 22:40:43 -04:00
813e9cb82e v21.1.1 2025-03-26 20:34:47 +00:00
3bcb3c1b2e Merge pull request #2925 from balena-io/add-overall-status
Add overall status
2025-03-26 20:33:52 +00:00
20d76556c2 Add explicit read properties for device command
Change-tye: minor
2025-03-26 17:12:45 -03:00
e829068725 Bump balena-sdk to 21.3.0
Update balena-sdk from 21.2.1 to 21.3.0

Change-type: patch
2025-03-26 17:07:20 -03:00
650e896f70 v21.1.0 2025-03-12 19:34:20 +00:00
a9042124ea Merge pull request #2922 from balena-io/compose-requirement-labels
Add support for requirement labels feature
2025-03-12 19:33:23 +00:00
d24d78dac7 Fix package release action for macOS
Depending on the signing password value, the script may interpret the
contents of the password and cause the signing process to fail.

This puts quotes around the password on assignment to prevent this.
2025-03-12 15:30:49 -03:00
42c50ef8ae Add support for new requirement labels feature
Updates @balena/compose to v7 to include this new feature.

See: https://balena.fibery.io/Work/Project/Refactoring-container-contracts-1205
Depends-on: https://github.com/balena-io-modules/balena-compose/pull/64
Change-type: minor
2025-03-12 15:30:21 -03:00
ba4b9bd447 v21.0.0 2025-03-11 14:42:31 +00:00
02c0ea5b59 Merge pull request #2921 from balena-io/major-21-second-attempt
Major 21
2025-03-11 10:41:28 -04:00
bc3558dd8e Address SDK major v21 breaking changes 2025-03-11 08:19:10 -04:00
aad62d1ccd Drop support for OS versions <2.14.0
Change-type: major
2025-03-11 08:19:10 -04:00
ecc6f80164 api-key generate: Add required argument expiryDate
Change-type: major
2025-03-11 08:19:10 -04:00
c0fd1e3886 Deduplicate dependencies 2025-03-11 08:18:56 -04:00
9d3120b144 Update balena-preload to 18.0.1
Change-type: patch
2025-03-11 08:17:27 -04:00
ed0e03ddb2 Add dependency date-fns
Change-type: patch
2025-03-11 08:16:50 -04:00
8fe6d6c026 Update balena-sdk to 21.2.1
Change-type: patch
2025-03-11 08:16:31 -04:00
727033ae14 v20.2.10 2025-03-10 17:33:13 +00:00
c19ce6a905 Merge pull request #2923 from balena-io/bump-typescript-to-5.8.2
Bump typescript to 5.8.2
2025-03-10 17:32:13 +00:00
1a33029738 Deduplicate dependencies 2025-03-10 17:51:18 +02:00
043bc48a1c Add a "deduplicate-dependencies" npm script to standardize such commits 2025-03-04 08:48:31 +02:00
a10156a441 Update TypeScript to 5.8.2
Change-type: patch
2025-03-04 08:48:31 +02:00
4f665f43d2 v20.2.9 2025-02-26 12:52:10 +00:00
9f097a96f5 Merge pull request #2920 from balena-io/x-balena-client-fix
Fix CORS issue with X-Balena-Client header
2025-02-26 12:51:20 +00:00
64d1943804 Fix CORS issue with X-Balena-Client header
Change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
2025-02-26 14:24:57 +02:00
666ce876e6 v20.2.8 2025-02-26 00:22:16 +00:00
e01184080f Merge pull request #2915 from balena-io/fix_os_configure_test
Update balena-config-json dependency and fix test
2025-02-25 19:21:25 -05:00
93039b010d Update balena-config-json dependency and fix test
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-02-25 18:43:12 -05:00
795259bf30 v20.2.7 2025-02-25 20:21:03 +00:00
fa134d2d39 Merge pull request #2917 from balena-io/x-balena-client
Use the CLI version in the X-Balena-Client header
2025-02-25 20:20:10 +00:00
bef5221ed8 Use the CLI version in the X-Balena-Client header
Change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
2025-02-25 21:59:35 +02:00
72d6db796c v20.2.6 2025-02-25 19:13:51 +00:00
e848eb63ee Merge pull request #2918 from balena-io/renovate/actions-upload-artifact-digest
Update actions/upload-artifact digest to 4cec3d8
2025-02-25 19:12:56 +00:00
6f0f7350cf Update actions/upload-artifact digest to 4cec3d8
Update actions/upload-artifact

Change-type: patch
2025-02-25 18:51:27 +00:00
07a88c700e v20.2.5 2025-02-25 18:10:50 +00:00
9cae66bd92 Merge pull request #2913 from balena-io/renovate/actions-setup-node-digest
Update actions/setup-node digest to 1d0ff46
2025-02-25 18:09:54 +00:00
cddea24cef Update actions/setup-node digest to 1d0ff46
Update actions/setup-node

Change-type: patch
2025-02-25 17:45:52 +00:00
b1c246c0b4 v20.2.4 2025-02-25 17:17:03 +00:00
00b4d57a03 Merge pull request #2916 from balena-io/pin_docker-modem_regression
Pin docker-modem and dockerode to avoid regression in docker-modem v5.0.6
2025-02-25 12:16:10 -05:00
2cba82e914 Pin docker-modem and dockerode to avoid regression
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-02-24 20:22:59 -05:00
22 changed files with 1141 additions and 526 deletions

View File

@ -39,7 +39,7 @@ runs:
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
@ -94,7 +94,7 @@ runs:
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_KEY_PASSWORD='${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}'
CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
@ -135,7 +135,7 @@ runs:
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
- name: Upload artifacts
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
path: dist

View File

@ -26,7 +26,7 @@ runs:
steps:
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
@ -58,7 +58,7 @@ runs:
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz

View File

@ -1,3 +1,249 @@
- commits:
- subject: Update dockerode/docker-modem dependencies for fixes
hash: b650f8ff6d01d2144886253f93aa1d1867f51980
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 21.1.5
title: ""
date: 2025-04-03T13:28:08.855Z
- commits:
- subject: Add comment with secure boot signature file example for preload
hash: 28703bb5ae13539ab4c1c597e6a53a5292a7edde
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 21.1.4
title: ""
date: 2025-04-02T09:16:27.791Z
- commits:
- subject: Fix device detail for open balena
hash: 0d4e411777dd53d83c475da3653ab94176e07d7d
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: 21.1.3
title: ""
date: 2025-03-28T16:57:00.250Z
- commits:
- subject: Deny preload for an image with secure boot enabled
hash: 7f2daeebb0973a59682ba4300e1b00bce6f6aead
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 21.1.2
title: ""
date: 2025-03-27T12:20:22.883Z
- commits:
- subject: Bump balena-sdk to 21.3.0
hash: e82906872538a7401e31bd52e662e8356a89d413
body: |
Update balena-sdk from 21.2.1 to 21.3.0
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested:
- commits:
- subject: "device: add `changed_api_heartbeat_state_on__date` to typings"
hash: bfa52cf58c7d06859a1b5c6f62ff7c71324d3d20
body: ""
footer:
Change-type: minor
change-type: minor
author: Otavio Jacobi
nested: []
version: balena-sdk-21.3.0
title: ""
date: 2025-03-26T19:54:35.558Z
- commits:
- subject: fix linting
hash: 13320aad81ba3dfc4950b9960f015b058222c4be
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: balena-sdk-21.2.2
title: ""
date: 2025-03-26T18:55:53.610Z
version: 21.1.1
title: ""
date: 2025-03-26T20:34:44.546Z
- commits:
- subject: Add support for new requirement labels feature
hash: 42c50ef8aed110b317a0472d928bf75e372b4c0b
body: |
Updates @balena/compose to v7 to include this new feature.
footer:
See: https://balena.fibery.io/Work/Project/Refactoring-container-contracts-1205
see: https://balena.fibery.io/Work/Project/Refactoring-container-contracts-1205
Depends-on: https://github.com/balena-io-modules/balena-compose/pull/64
depends-on: https://github.com/balena-io-modules/balena-compose/pull/64
Change-type: minor
change-type: minor
author: Felipe Lalanne
nested: []
version: 21.1.0
title: ""
date: 2025-03-12T19:34:17.610Z
- commits:
- subject: Drop support for OS versions <2.14.0
hash: aad62d1ccd11ebb69b1035d5b95aef93d384bfd5
body: ""
footer:
Change-type: major
change-type: major
author: myarmolinsky
nested: []
- subject: "api-key generate: Add required argument `expiryDate`"
hash: ecc6f80164fca3c0cde42b140b6d7404abe8c877
body: ""
footer:
Change-type: major
change-type: major
author: myarmolinsky
nested: []
- subject: Update `balena-preload` to 18.0.1
hash: 9d3120b144c2c017eda55463b034f1561d264213
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
- subject: Add dependency `date-fns`
hash: ed0e03ddb274da294f719dc0e307ec37591e10d7
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
- subject: Update `balena-sdk` to 21.2.1
hash: 8fe6d6c0268f69bcf3bcac3c57470272b959e9b0
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
version: 21.0.0
title: ""
date: 2025-03-11T14:42:28.479Z
- commits:
- subject: Update TypeScript to 5.8.2
hash: a10156a441b737275cabfb03bd10bfc5aba7bc88
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: 20.2.10
title: ""
date: 2025-03-10T17:33:09.548Z
- commits:
- subject: Fix CORS issue with X-Balena-Client header
hash: 64d19438042921e89c522f022327ead85b286e9f
body: ""
footer:
Change-type: patch
change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
see: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
author: Thodoris Greasidis
nested: []
version: 20.2.9
title: ""
date: 2025-02-26T12:52:06.672Z
- commits:
- subject: Update balena-config-json dependency and fix test
hash: 93039b010db15fbf1c0d17d4ed8f0db554064de4
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 20.2.8
title: ""
date: 2025-02-26T00:22:14.010Z
- commits:
- subject: Use the CLI version in the X-Balena-Client header
hash: bef5221ed891db12a0b760f12fc9654e2f4e241b
body: ""
footer:
Change-type: patch
change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
see: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
author: Thodoris Greasidis
nested: []
version: 20.2.7
title: ""
date: 2025-02-25T20:21:00.603Z
- commits:
- subject: Update actions/upload-artifact digest to 4cec3d8
hash: 6f0f7350cf65c35abd099a901266821c218478eb
body: |
Update actions/upload-artifact
footer:
Change-type: patch
change-type: patch
author: balena-renovate[bot]
nested: []
version: 20.2.6
title: ""
date: 2025-02-25T19:13:48.297Z
- commits:
- subject: Update actions/setup-node digest to 1d0ff46
hash: cddea24cefdfef475731e0a7d2bdec4992959a6b
body: |
Update actions/setup-node
footer:
Change-type: patch
change-type: patch
author: balena-renovate[bot]
nested: []
version: 20.2.5
title: ""
date: 2025-02-25T18:10:47.617Z
- commits:
- subject: Pin docker-modem and dockerode to avoid regression
hash: 2cba82e914c720e75b68bd4370a2a92b4d4a7ba0
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 20.2.4
title: ""
date: 2025-02-25T17:17:00.607Z
- commits:
- subject: Remove unused old eslint version files
hash: 53be743b9dcecbb2f0d8841b6663e7af1a9006c3

View File

@ -4,6 +4,79 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## 21.1.5 - 2025-04-03
* Update dockerode/docker-modem dependencies for fixes [Ken Bannister]
## 21.1.4 - 2025-04-02
* Add comment with secure boot signature file example for preload [Ken Bannister]
## 21.1.3 - 2025-03-28
* Fix device detail for open balena [Otavio Jacobi]
## 21.1.2 - 2025-03-27
* Deny preload for an image with secure boot enabled [Ken Bannister]
## 21.1.1 - 2025-03-26
<details>
<summary> Bump balena-sdk to 21.3.0 [Otavio Jacobi] </summary>
> ### balena-sdk-21.3.0 - 2025-03-26
>
> * device: add `changed_api_heartbeat_state_on__date` to typings [Otavio Jacobi]
>
> ### balena-sdk-21.2.2 - 2025-03-26
>
> * fix linting [Otavio Jacobi]
>
</details>
## 21.1.0 - 2025-03-12
* Add support for new requirement labels feature [Felipe Lalanne]
## 21.0.0 - 2025-03-11
* Drop support for OS versions <2.14.0 [myarmolinsky]
* api-key generate: Add required argument `expiryDate` [myarmolinsky]
* Update `balena-preload` to 18.0.1 [myarmolinsky]
* Add dependency `date-fns` [myarmolinsky]
* Update `balena-sdk` to 21.2.1 [myarmolinsky]
## 20.2.10 - 2025-03-10
* Update TypeScript to 5.8.2 [Thodoris Greasidis]
## 20.2.9 - 2025-02-26
* Fix CORS issue with X-Balena-Client header [Thodoris Greasidis]
## 20.2.8 - 2025-02-26
* Update balena-config-json dependency and fix test [Ken Bannister]
## 20.2.7 - 2025-02-25
* Use the CLI version in the X-Balena-Client header [Thodoris Greasidis]
## 20.2.6 - 2025-02-25
* Update actions/upload-artifact digest to 4cec3d8 [balena-renovate[bot]]
## 20.2.5 - 2025-02-25
* Update actions/setup-node digest to 1d0ff46 [balena-renovate[bot]]
## 20.2.4 - 2025-02-25
* Pin docker-modem and dockerode to avoid regression [Ken Bannister]
## 20.2.3 - 2025-01-15
* Remove unused old eslint version files [Otavio Jacobi]

View File

@ -326,6 +326,8 @@ or to authenticate requests to the API with an 'Authorization: Bearer <key>' hea
Examples:
$ balena api-key generate "Jenkins Key"
$ balena api-key generate "Jenkins Key" 2025-10-30
$ balena api-key generate "Jenkins Key" never
### Arguments
@ -333,6 +335,10 @@ Examples:
the API key name
#### EXPIRYDATE
the expiry date of the API key as an ISO date string, or "never" for no expiry
## api-key list
### Aliases

867
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "20.2.3",
"version": "21.1.5",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -58,6 +58,7 @@
"build:completion": "node completion/generate-completion.js",
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
"build:installer": "ts-node --transpile-only automation/run.ts build:installer",
"deduplicate-dependencies": "npm dd && git add npm-shrinkwrap.json && git commit --message \"Deduplicate dependencies\"",
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
"pretest": "npm run build",
"test": "npm run test:shrinkwrap && npm run test:core",
@ -186,21 +187,21 @@
"sinon": "^19.0.0",
"string-to-stream": "^3.0.1",
"ts-node": "^10.4.0",
"typescript": "^5.7.2"
"typescript": "^5.8.2"
},
"dependencies": {
"@balena/compose": "^6.0.0",
"@balena/compose": "^7.0.1",
"@balena/dockerignore": "^1.0.2",
"@balena/env-parsing": "^1.1.8",
"@balena/es-version": "^1.0.1",
"@oclif/core": "^4.1.0",
"@sentry/node": "^6.16.1",
"balena-config-json": "^4.2.0",
"balena-config-json": "^4.2.2",
"balena-device-init": "^8.1.3",
"balena-errors": "^4.7.3",
"balena-image-fs": "^7.3.0",
"balena-preload": "^17.0.0",
"balena-sdk": "^20.8.0",
"balena-image-fs": "^7.5.0",
"balena-preload": "^18.0.1",
"balena-sdk": "^21.3.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^5.0.2",
"balena-settings-storage": "^8.1.0",
@ -211,10 +212,11 @@
"cli-truncate": "^2.1.0",
"color-hash": "^1.1.1",
"common-tags": "^1.7.2",
"date-fns": "^4.1.0",
"denymount": "^2.3.0",
"docker-modem": "^5.0.3",
"docker-modem": "^5.0.6",
"docker-progress": "^5.1.3",
"dockerode": "^4.0.2",
"dockerode": "^4.0.5",
"ejs": "^3.1.6",
"etcher-sdk": "9.1.0",
"express": "^4.17.2",
@ -274,6 +276,6 @@
}
},
"versionist": {
"publishedAt": "2025-01-15T18:21:03.702Z"
"publishedAt": "2025-04-03T13:28:09.802Z"
}
}

View File

@ -17,7 +17,16 @@
import { Args, Command } from '@oclif/core';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
import {
formatDuration,
intervalToDuration,
isValid,
parseISO,
} from 'date-fns';
// In days
const durations = [1, 7, 30, 90];
async function isLoggedInWithJwt() {
const balena = getBalenaSdk();
@ -41,13 +50,21 @@ export default class GenerateCmd extends Command {
This key can be used to log into the CLI using 'balena login --token <key>',
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
`;
public static examples = ['$ balena api-key generate "Jenkins Key"'];
public static examples = [
'$ balena api-key generate "Jenkins Key"',
'$ balena api-key generate "Jenkins Key" 2025-10-30',
'$ balena api-key generate "Jenkins Key" never',
];
public static args = {
name: Args.string({
description: 'the API key name',
required: true,
}),
expiryDate: Args.string({
description:
'the expiry date of the API key as an ISO date string, or "never" for no expiry',
}),
};
public static authenticated = true;
@ -55,9 +72,61 @@ export default class GenerateCmd extends Command {
public async run() {
const { args: params } = await this.parse(GenerateCmd);
let expiryDateResponse: string | number | undefined = params.expiryDate;
let key;
try {
key = await getBalenaSdk().models.apiKey.create(params.name);
if (!expiryDateResponse) {
expiryDateResponse = await getCliForm().ask({
message: 'Please pick an expiry date for the API key',
type: 'list',
choices: [...durations, 'custom', 'never'].map((duration) => ({
name:
duration === 'never'
? 'No expiration'
: typeof duration === 'number'
? formatDuration(
intervalToDuration({
start: 0,
end: duration * 24 * 60 * 60 * 1000,
}),
)
: 'Custom expiration',
value: duration,
})),
});
}
let expiryDate: Date | null;
if (expiryDateResponse === 'never') {
expiryDate = null;
} else if (expiryDateResponse === 'custom') {
do {
expiryDate = parseISO(
await getCliForm().ask({
message:
'Please enter an expiry date for the API key as an ISO date string',
type: 'input',
}),
);
if (!isValid(expiryDate)) {
console.error('Invalid date format');
}
} while (!isValid(expiryDate));
} else if (typeof expiryDateResponse === 'string') {
expiryDate = parseISO(expiryDateResponse);
if (!isValid(expiryDate)) {
throw new Error(
'Invalid date format, please use a valid ISO date string',
);
}
} else {
expiryDate = new Date(
Date.now() + expiryDateResponse * 24 * 60 * 60 * 1000,
);
}
key = await getBalenaSdk().models.apiKey.create({
name: params.name,
expiryDate: expiryDate === null ? null : expiryDate.toISOString(),
});
} catch (e) {
if (e.name === 'BalenaNotLoggedIn') {
if (await isLoggedInWithJwt()) {

View File

@ -368,6 +368,7 @@ ${dockerignoreHelp}
!opts.shouldUploadLogs,
composeOpts.projectPath,
opts.createAsDraft,
project.descriptors,
);
}

View File

@ -77,45 +77,59 @@ export default class DeviceCmd extends Command {
const balena = getBalenaSdk();
const device = (await balena.models.device.get(
params.uuid,
options.json
? {
$expand: {
device_tag: {
$select: ['tag_key', 'value'],
},
...expandForAppName.$expand,
let device: ExtendedDevice;
if (options.json) {
const [deviceBase, deviceComputed] = await Promise.all([
balena.models.device.get(params.uuid, {
$expand: {
device_tag: {
$select: ['tag_key', 'value'],
},
}
: {
$select: [
'device_name',
'id',
'overall_status',
'is_online',
'ip_address',
'mac_address',
'last_connectivity_event',
'uuid',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
'cpu_usage',
'cpu_temp',
'cpu_id',
'is_undervolted',
],
...expandForAppName,
...expandForAppName.$expand,
},
)) as ExtendedDevice;
}),
balena.models.device.get(params.uuid, {
$select: [
'overall_status',
'overall_progress',
'should_be_running__release',
],
}),
]);
device = {
...deviceBase,
...deviceComputed,
} as ExtendedDevice;
} else {
device = (await balena.models.device.get(params.uuid, {
$select: [
'device_name',
'id',
'overall_status',
'is_online',
'ip_address',
'mac_address',
'last_connectivity_event',
'uuid',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
'cpu_usage',
'cpu_temp',
'cpu_id',
'is_undervolted',
],
...expandForAppName,
})) as ExtendedDevice;
}
if (options.view) {
const open = await import('open');

View File

@ -37,6 +37,7 @@ import type {
Release,
} from 'balena-sdk';
import type { Preloader } from 'balena-preload';
import type * as Fs from 'fs';
export default class PreloadCmd extends Command {
public static description = stripIndent`
@ -161,6 +162,42 @@ Can be repeated to add multiple certificates.\
);
}
// Verify that image is not enabled for secure boot. First, confirm it is
// a secure boot image with a .sig file in the /opt directory of the rootA
// partition. For example, below are contents for generic-amd64 device type:
// $ ls -l opt
// total 864696
// -rw-r--r-- 1 root root 2378170368 Mar 26 09:14 balena-image-generic-amd64.balenaos-img
// -rw-r--r-- 1 root root 512 Mar 9 2018 balena-image-generic-amd64.balenaos-img.sig
const { explorePartition, BalenaPartition } = await import(
'../../utils/image-contents'
);
const isSecureBoot = await explorePartition<boolean>(
params.image,
BalenaPartition.ROOTA,
async (fs: typeof Fs): Promise<boolean> => {
try {
const files = await fs.promises.readdir('/opt');
return files.some((el) => el.endsWith('balenaos-img.sig'));
} catch {
// Typically one of:
// - Error: No such file or directory
// - Error: Unsupported filesystem.
// - ErrnoException: node_ext2fs_open ENOENT (44) args: [5261576,5268064,"r",0]
return false;
}
return false;
},
);
// Next verify that config.json enables secureboot.
if (isSecureBoot) {
const { read } = await import('balena-config-json');
const config = await read(params.image, '');
if (config.installer?.secureboot === true) {
throw new ExpectedError("Can't preload image with secure boot enabled");
}
}
// balena-preload currently does not work with numerical app IDs
// Load app here, and use app slug from hereon
const fleetSlug: string | undefined = options.fleet
@ -295,7 +332,7 @@ Can be repeated to add multiple certificates.\
owns__release: {
$select: ['id', 'commit', 'end_timestamp', 'composition'],
$expand: {
contains__image: {
release_image: {
$select: ['image'],
$expand: {
image: {

View File

@ -128,6 +128,7 @@ export const createRelease = async function (
draft: boolean,
semver: string | undefined,
contract: import('@balena/compose/dist/release/models').ReleaseModel['contract'],
imgDescriptors: ImageDescriptor[],
): Promise<Release> {
const _ = require('lodash') as typeof import('lodash');
const crypto = require('crypto') as typeof import('crypto');
@ -167,6 +168,7 @@ export const createRelease = async function (
semver,
is_final: !draft,
contract,
imgDescriptors,
});
return {
@ -240,7 +242,7 @@ export const getPreviousRepos = (
status: 'success',
},
$expand: {
contains__image: {
release_image: {
$select: 'image',
$expand: { image: { $select: 'is_stored_at__image_location' } },
},
@ -252,7 +254,7 @@ export const getPreviousRepos = (
.then(function (release) {
// grab all images from the latest release, return all image locations in the registry
if (release.length > 0) {
const images = release[0].contains__image as Array<{
const images = release[0].release_image as Array<{
image: [SDK.Image];
}>;
const { getRegistryAndName } =

View File

@ -1375,6 +1375,7 @@ export async function deployProject(
skipLogUpload: boolean,
projectPath: string,
isDraft: boolean,
imgDescriptors: ImageDescriptor[],
): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
const releaseMod = await import('@balena/compose/dist/release');
const { createRelease, tagServiceImages } = await import('./compose');
@ -1405,6 +1406,7 @@ export async function deployProject(
isDraft,
contract?.version,
contract,
imgDescriptors,
),
);
const { client: pineClient, release, serviceImages } = $release;

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type * as BalenaSdk from 'balena-sdk';
import * as semver from 'balena-semver';
import { getBalenaSdk, stripIndent } from './lazy';
export interface ImgConfig {
@ -122,16 +121,10 @@ export function generateDeviceConfig(
// os.getConfig always returns a config for an app
delete config.apiKey;
if (deviceApiKey == null && semver.satisfies(options.version, '<2.0.3')) {
config.apiKey = await sdk.models.application.generateApiKey(
application.id,
);
} else {
config.deviceApiKey =
typeof deviceApiKey === 'string' && deviceApiKey
? deviceApiKey
: await sdk.models.device.generateDeviceKey(device.uuid);
}
config.deviceApiKey =
typeof deviceApiKey === 'string' && deviceApiKey
? deviceApiKey
: await sdk.models.device.generateDeviceKey(device.uuid);
return config;
})

View File

@ -0,0 +1,69 @@
/**
* @license
* Copyright 2025 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.
*/
// Utilities to explore the contents in a balenaOS image.
import * as imagefs from 'balena-image-fs';
import * as filedisk from 'file-disk';
import { getPartitions } from 'partitioninfo';
import type * as Fs from 'fs';
/**
* @summary IDs for the standard balenaOS partitions
* @description Values are the base name for a partition on disk
*/
export enum BalenaPartition {
BOOT = 'boot',
ROOTA = 'rootA',
ROOTB = 'rootB',
STATE = 'state',
DATA = 'data',
}
/**
* @summary Allow a provided function to explore the contents of one of the well-known
* partitions of a balenaOS image
*
* @param {string} imagePath - pathname of image for search
* @param {BalenaPartition} partitionId - partition to find
* @param {(fs) => Promise<T>} - function for exploration
* @returns {T}
*/
export async function explorePartition<T>(
imagePath: string,
partitionId: BalenaPartition,
exploreFn: (fs: typeof Fs) => Promise<T>,
): Promise<T> {
return await filedisk.withOpenFile(imagePath, 'r', async (handle) => {
const disk = new filedisk.FileDisk(handle, true, false, false);
const partitionInfo = await getPartitions(disk, {
includeExtended: false,
getLogical: true,
});
const findResult = await imagefs.findPartition(disk, partitionInfo, [
`resin-${partitionId}`,
`flash-${partitionId}`,
`balena-${partitionId}`,
]);
if (findResult == null) {
throw new Error(`Can't find partition for ${partitionId}`);
}
return await imagefs.interact<T>(disk, findResult.index, exploreFn);
});
}

View File

@ -76,38 +76,28 @@ export const getImagePath = async (deviceType: string, version?: string) => {
};
/**
* @summary Determine if a device image is fresh
* @summary Determine if a device image is cached
*
* @description
* If the device image does not exist, return false.
*
* @param {String} deviceType - device type slug or alias
* @param {String} version - the exact balenaOS version number
* @returns {Promise<Boolean>} is image fresh
* @returns {Promise<Boolean>} is image cached
*
* @example
* isImageFresh('raspberry-pi', '1.2.3').then (isFresh) ->
* if isFresh
* console.log('The Raspberry Pi image v1.2.3 is fresh!')
* isImageCached ('raspberry-pi', '1.2.3').then (isCached) ->
* if isCached
* console.log('The Raspberry Pi image v1.2.3 is cached!')
*/
export const isImageFresh = async (deviceType: string, version: string) => {
export const isImageCached = async (deviceType: string, version: string) => {
const imagePath = await getImagePath(deviceType, version);
let createdDate;
try {
createdDate = await getFileCreatedDate(imagePath);
const createdDate = await getFileCreatedDate(imagePath);
return createdDate != null;
} catch {
// Swallow errors from getFileCreatedTime.
}
if (createdDate == null) {
return false;
}
const balena = getBalenaSdk();
const lastModifiedDate = await balena.models.os.getLastModified(
deviceType,
version,
);
return lastModifiedDate < createdDate;
};
/**
@ -286,7 +276,7 @@ export const getStream = async (
versionOrRange = 'latest';
}
const version = await resolveVersion(deviceType, versionOrRange);
const isFresh = await isImageFresh(deviceType, version);
const isFresh = await isImageCached(deviceType, version);
const $stream = isFresh
? await getImage(deviceType, version)
: await doDownload({ ...options, deviceType, version });

View File

@ -21,6 +21,7 @@ import type { Chalk } from 'chalk';
import type * as visuals from 'resin-cli-visuals';
import type * as CliForm from 'resin-cli-form';
import type { ux } from '@oclif/core';
import { version } from '../../package.json';
// Equivalent of _.once but avoiding the need to import lodash for lazy deps
const once = <T>(fn: () => T) => {
@ -43,9 +44,26 @@ export const onceAsync = <T>(fn: () => Promise<T>) => {
};
};
export const getBalenaSdk = once(() =>
(require('balena-sdk') as typeof BalenaSdk).fromSharedOptions(),
);
const cliXBalenaClientHeaderInterceptor: BalenaSdk.Interceptor = {
request($request) {
if ($request.headers['X-Balena-Client']) {
// We intentionally overwrite the sdk version string from the header
// to conserve bandwidth. We only do that when the SDK already has specified
// the X-Balena-Client header, since that signals that this is a safe url to
// include the extra header and will not cause CORS errors.
$request.headers['X-Balena-Client'] = `balena-cli/${version}`;
}
return $request;
},
};
export const getBalenaSdk = once(() => {
const sdk = (require('balena-sdk') as typeof BalenaSdk).fromSharedOptions();
if (!sdk.interceptors.includes(cliXBalenaClientHeaderInterceptor)) {
sdk.interceptors.push(cliXBalenaClientHeaderInterceptor);
}
return sdk;
});
export const getVisuals = once(
() => require('resin-cli-visuals') as typeof visuals,

View File

@ -114,6 +114,14 @@ describe('balena device', function () {
'Content-Type': 'application/json',
});
api.scope
.get(
/^\/v\d+\/device\?.+&\$select=overall_status,overall_progress,should_be_running__release$/,
)
.replyWithFile(200, path.join(apiResponsePath, 'device.json'), {
'Content-Type': 'application/json',
});
const { out, err } = await runCommand('device 27fda508c --json');
expect(err).to.be.empty;
const json = JSON.parse(out.join(''));

View File

@ -231,6 +231,10 @@ if (process.platform !== 'win32') {
err.flatMap((line) => line.split('\n')).filter((line) => line !== ''),
).to.deep.equal(
stripIndent`
[warn] "${tmpDummyPath}":
[warn] Found partition table with 1 partitions,
[warn] but none with a name/label in ['resin-boot', 'flash-boot', 'balena-boot'].
[warn] Will scan all partitions for contents.
[warn] "${tmpDummyPath}":
[warn] 1 partition(s) found, but none containing file "/device-type.json".
[warn] Assuming default boot partition number '1'.

View File

@ -82,7 +82,7 @@ describe('balena release', function () {
expect(err).to.be.empty;
const json = JSON.parse(out.join(''));
expect(json[0].commit).to.equal('90247b54de4fa7a0a3cbc85e73c68039');
expect(json[0].contains__image[0].image[0].start_timestamp).to.equal(
expect(json[0].release_image[0].image[0].start_timestamp).to.equal(
'2020-01-04T01:13:08.583Z',
);
});

View File

@ -10,7 +10,7 @@
"build_log": null,
"start_timestamp": "2021-08-25T22:18:33.624Z",
"end_timestamp": "2021-08-25T22:18:48.820Z",
"contains__image": [
"release_image": [
{
"image": [
{

View File

@ -42,7 +42,7 @@ describe('image-manager', function () {
describe('given the image is fresh', function () {
beforeEach(function () {
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
return this.cacheIsImageFresh.resolves(true);
});
@ -68,7 +68,7 @@ describe('image-manager', function () {
describe('given the image is not fresh', function () {
beforeEach(function () {
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
return this.cacheIsImageFresh.resolves(false);
});
@ -280,7 +280,7 @@ describe('image-manager', function () {
});
});
describe('.isImageFresh()', () => {
describe('.isImageCached()', () => {
describe('given the raspberry-pi manifest', function () {
beforeEach(function () {
this.getDeviceTypeManifestBySlugStub = stub(
@ -314,78 +314,8 @@ describe('image-manager', function () {
});
it('should return false', async function () {
expect(await imageManager.isImageFresh('raspberry-pi', '1.2.3')).to.be
.false;
});
});
describe('given a fixed created time', function () {
beforeEach(function () {
this.utilsGetFileCreatedDate = stub(
imageManager,
'getFileCreatedDate',
);
this.utilsGetFileCreatedDate.resolves(
new Date('2014-01-01T00:00:00.000Z'),
);
});
afterEach(function () {
this.utilsGetFileCreatedDate.restore();
});
describe('given the file was created before the os last modified time', function () {
beforeEach(function () {
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
this.osGetLastModified.resolves(
new Date('2014-02-01T00:00:00.000Z'),
);
});
afterEach(function () {
this.osGetLastModified.restore();
});
it('should return false', function () {
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
return expect(promise).to.eventually.be.false;
});
});
describe('given the file was created after the os last modified time', function () {
beforeEach(function () {
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
this.osGetLastModified.resolves(
new Date('2013-01-01T00:00:00.000Z'),
);
});
afterEach(function () {
this.osGetLastModified.restore();
});
it('should return true', function () {
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
return expect(promise).to.eventually.be.true;
});
});
describe('given the file was created just at the os last modified time', function () {
beforeEach(function () {
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
this.osGetLastModified.resolves(
new Date('2014-00-01T00:00:00.000Z'),
);
});
afterEach(function () {
this.osGetLastModified.restore();
});
it('should return false', function () {
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
return expect(promise).to.eventually.be.false;
});
expect(await imageManager.isImageCached('raspberry-pi', '1.2.3')).to
.be.false;
});
});
});