mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
2 Commits
master
...
native-bui
Author | SHA1 | Date | |
---|---|---|---|
0900db5b21 | |||
4156e71c38 |
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/completion/*
|
||||||
|
/bin/*
|
21
.eslintrc.js
Normal file
21
.eslintrc.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['./node_modules/@balena/lint/config/.eslintrc.js'],
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.dev.json',
|
||||||
|
},
|
||||||
|
root: true,
|
||||||
|
rules: {
|
||||||
|
ignoreDefinitionFiles: 0,
|
||||||
|
// to avoid the `warning Forbidden non-null assertion @typescript-eslint/no-non-null-assertion`
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||||
|
},
|
||||||
|
};
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -67,7 +67,7 @@ fixed it.
|
|||||||
- **Cloud backend: openBalena or balenaCloud?** If unsure, it will be balenaCloud
|
- **Cloud backend: openBalena or balenaCloud?** If unsure, it will be balenaCloud
|
||||||
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
|
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
|
||||||
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
|
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
|
||||||
- **Install method:** npm or standalone package or executable installer
|
- **Install method:** npm or zip package or executable installer
|
||||||
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
|
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
|
||||||
|
|
||||||
# Additional References
|
# Additional References
|
||||||
|
14
.github/actions/publish/action.yml
vendored
14
.github/actions/publish/action.yml
vendored
@ -18,7 +18,7 @@ inputs:
|
|||||||
default: 'accounts+apple@balena.io'
|
default: 'accounts+apple@balena.io'
|
||||||
NODE_VERSION:
|
NODE_VERSION:
|
||||||
type: string
|
type: string
|
||||||
default: '22.x'
|
default: '20.x'
|
||||||
VERBOSE:
|
VERBOSE:
|
||||||
type: string
|
type: string
|
||||||
default: 'true'
|
default: 'true'
|
||||||
@ -28,7 +28,7 @@ runs:
|
|||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Download custom source artifact
|
- name: Download custom source artifact
|
||||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||||
with:
|
with:
|
||||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||||
path: ${{ runner.temp }}
|
path: ${{ runner.temp }}
|
||||||
@ -39,7 +39,7 @@ runs:
|
|||||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.NODE_VERSION }}
|
node-version: ${{ inputs.NODE_VERSION }}
|
||||||
cache: npm
|
cache: npm
|
||||||
@ -48,7 +48,7 @@ runs:
|
|||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Install additional tools
|
- name: Install additional tools
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
@ -94,7 +94,7 @@ runs:
|
|||||||
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
|
||||||
if [[ $runner_os =~ darwin|macos|osx ]]; then
|
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_KEYCHAIN=signing_temp
|
||||||
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||||
|
|
||||||
@ -112,8 +112,8 @@ runs:
|
|||||||
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
|
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
|
||||||
smksp_registrar.exe list
|
smksp_registrar.exe list
|
||||||
smctl.exe keypair ls
|
smctl.exe keypair ls
|
||||||
smctl.exe windows certsync
|
|
||||||
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
||||||
|
smksp_cert_sync.exe
|
||||||
|
|
||||||
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
|
# (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}"
|
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
|
||||||
@ -135,7 +135,7 @@ runs:
|
|||||||
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
|
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||||
with:
|
with:
|
||||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
|
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
|
||||||
path: |
|
path: |
|
||||||
|
6
.github/actions/test/action.yml
vendored
6
.github/actions/test/action.yml
vendored
@ -15,7 +15,7 @@ inputs:
|
|||||||
# --- custom environment
|
# --- custom environment
|
||||||
NODE_VERSION:
|
NODE_VERSION:
|
||||||
type: string
|
type: string
|
||||||
default: '22.x'
|
default: '20.x'
|
||||||
VERBOSE:
|
VERBOSE:
|
||||||
type: string
|
type: string
|
||||||
default: "true"
|
default: "true"
|
||||||
@ -26,7 +26,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.NODE_VERSION }}
|
node-version: ${{ inputs.NODE_VERSION }}
|
||||||
cache: npm
|
cache: npm
|
||||||
@ -58,7 +58,7 @@ runs:
|
|||||||
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
||||||
|
|
||||||
- name: Upload custom artifact
|
- name: Upload custom artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||||
with:
|
with:
|
||||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||||
path: ${{ runner.temp }}/custom.tgz
|
path: ${{ runner.temp }}/custom.tgz
|
||||||
|
File diff suppressed because it is too large
Load Diff
379
CHANGELOG.md
379
CHANGELOG.md
@ -4,385 +4,6 @@ All notable changes to this project will be documented in this file
|
|||||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## 22.1.1 - 2025-06-19
|
|
||||||
|
|
||||||
* Deploy: Limit the submitted error_message of images that fail to build to 1000 characters [Thodoris Greasidis]
|
|
||||||
|
|
||||||
## 22.1.0 - 2025-06-09
|
|
||||||
|
|
||||||
* Add support for node 22 [Otavio Jacobi]
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary> Bump etcher-sdk to v10.0.0 [Otavio Jacobi] </summary>
|
|
||||||
|
|
||||||
> ### etcher-sdk-10.0.0 - 2025-06-02
|
|
||||||
>
|
|
||||||
> * Drop support to node18 and add support to node 22 & 24 [Otavio Jacobi]
|
|
||||||
>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 22.0.6 - 2025-06-02
|
|
||||||
|
|
||||||
* Remove `request` dependency [myarmolinsky]
|
|
||||||
* Replace `request` usage with `got` [myarmolinsky]
|
|
||||||
|
|
||||||
## 22.0.5 - 2025-05-29
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary> Bump etcher-sdk to v9.1.4 [Otavio Jacobi] </summary>
|
|
||||||
|
|
||||||
> ### etcher-sdk-9.1.4 - 2025-05-29
|
|
||||||
>
|
|
||||||
> * Run `npm audit fix` which should only do non-breaking changes [Otavio Jacobi]
|
|
||||||
>
|
|
||||||
> ### etcher-sdk-9.1.3 - 2025-02-17
|
|
||||||
>
|
|
||||||
> * Embed config.json with a fixed timestamp to enable consistent checksums [Pagan Gazzard]
|
|
||||||
>
|
|
||||||
> ### etcher-sdk-9.1.2 - 2024-10-09
|
|
||||||
>
|
|
||||||
> * Update dependency unzip-stream to v0.3.2 [SECURITY] [Self-hosted Renovate Bot]
|
|
||||||
>
|
|
||||||
> ### etcher-sdk-9.1.1 - 2024-10-09
|
|
||||||
>
|
|
||||||
> * patch: add EXLOCK flag for windows [Talha Can Havadar]
|
|
||||||
>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 22.0.4 - 2025-05-29
|
|
||||||
|
|
||||||
* tests: Replace request with got [Otavio Jacobi]
|
|
||||||
* deploy-legacy: Replace request with got [Otavio Jacobi]
|
|
||||||
|
|
||||||
## 22.0.3 - 2025-05-29
|
|
||||||
|
|
||||||
* Bump sentry to v9 [Otavio Jacobi]
|
|
||||||
|
|
||||||
## 22.0.2 - 2025-05-28
|
|
||||||
|
|
||||||
* Fix balena build to work with --nologs [Otavio Jacobi]
|
|
||||||
|
|
||||||
## 22.0.1 - 2025-05-28
|
|
||||||
|
|
||||||
* DeviceAPI: Move away from `request` in favor of BalenaSdk request [myarmolinsky]
|
|
||||||
* Update `nock` to 14.0.4 [myarmolinsky]
|
|
||||||
|
|
||||||
## 22.0.0 - 2025-05-26
|
|
||||||
|
|
||||||
* Add migration guide to v22 [Otavio Jacobi]
|
|
||||||
* Build standalone without pkg [Otavio Jacobi]
|
|
||||||
|
|
||||||
## 21.1.14 - 2025-05-21
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary> Bump balena-preload to 18.0.4 [Otavio Jacobi] </summary>
|
|
||||||
|
|
||||||
> ### balena-preload-18.0.4 - 2025-05-21
|
|
||||||
>
|
|
||||||
> * Fix balena-preload pip deps [Otavio Jacobi]
|
|
||||||
>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 21.1.13 - 2025-05-21
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary> Bump balena-preload to 18.0.3 [Otavio Jacobi] </summary>
|
|
||||||
|
|
||||||
> ### balena-preload-18.0.3 - 2025-03-19
|
|
||||||
>
|
|
||||||
> * Update dependency sh to v1.14.3 [balena-renovate[bot]]
|
|
||||||
>
|
|
||||||
> ### balena-preload-18.0.2 - 2025-03-19
|
|
||||||
>
|
|
||||||
> * Update alpine Docker tag to v3.21 [balena-renovate[bot]]
|
|
||||||
>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 21.1.12 - 2025-05-16
|
|
||||||
|
|
||||||
* Update @balena/compose dependency to fix error with build secrets. [Ken Bannister]
|
|
||||||
|
|
||||||
## 21.1.11 - 2025-05-06
|
|
||||||
|
|
||||||
* Fix typos in `os configure` and `config generate` help messages [myarmolinsky]
|
|
||||||
|
|
||||||
## 21.1.10 - 2025-05-05
|
|
||||||
|
|
||||||
* patch: fix windows signing [Edwin Joassart]
|
|
||||||
|
|
||||||
## 21.1.9 - 2025-04-07
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary> Update balena-config-json to rely on the balena-image-fs helpers [Thodoris Greasidis] </summary>
|
|
||||||
|
|
||||||
> ### balena-config-json-4.2.7 - 2025-04-02
|
|
||||||
>
|
|
||||||
> * Fix getBootPartition always warning that the boot partitions were not found [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
> ### balena-config-json-4.2.6 - 2025-04-01
|
|
||||||
>
|
|
||||||
> * write: Allow undefined as a value for the deprecated type parameter [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
> <details>
|
|
||||||
> <summary> Use the balena-image-fs findPartition() helper to find the boot partition [Thodoris Greasidis] </summary>
|
|
||||||
>
|
|
||||||
>> #### balena-image-fs-7.5.0 - 2025-03-26
|
|
||||||
>>
|
|
||||||
>> * Add function to find a partition by name/label [Ken Bannister]
|
|
||||||
>>
|
|
||||||
>> #### balena-image-fs-7.4.1 - 2025-02-21
|
|
||||||
>>
|
|
||||||
>> * bump ext2fs to 4.2.4 [Ryan Cooke]
|
|
||||||
>>
|
|
||||||
>
|
|
||||||
> </details>
|
|
||||||
>
|
|
||||||
>
|
|
||||||
> ### balena-config-json-4.2.5 - 2025-03-26
|
|
||||||
>
|
|
||||||
> * Update @balena/lint to 9.1.4 [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
> ### balena-config-json-4.2.4 - 2025-03-26
|
|
||||||
>
|
|
||||||
> * Switch use ts-mocha instead of manually compiling the tests [Thodoris Greasidis]
|
|
||||||
> * Update TypeScript to 5.8.2 [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
> ### balena-config-json-4.2.3 - 2025-03-26
|
|
||||||
>
|
|
||||||
> * Update chai to v5 [balena-renovate[bot]]
|
|
||||||
>
|
|
||||||
> ### balena-image-fs-7.5.2 - 2025-04-02
|
|
||||||
>
|
|
||||||
> * Update dependency jsdoc-to-markdown to v9 [balena-renovate[bot]]
|
|
||||||
>
|
|
||||||
> ### balena-image-fs-7.5.1 - 2025-03-26
|
|
||||||
>
|
|
||||||
> * Update @balena/lint to v9 [Thodoris Greasidis]
|
|
||||||
> * Remove the no longer needed gpt detection helper [Thodoris Greasidis]
|
|
||||||
> * Update TypeScript to 5.8.2 [Thodoris Greasidis]
|
|
||||||
> * Drop the package-lock.json [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 21.1.8 - 2025-04-03
|
|
||||||
|
|
||||||
* Update actions/download-artifact action to v4.1.9 [balena-renovate[bot]]
|
|
||||||
|
|
||||||
## 21.1.7 - 2025-04-03
|
|
||||||
|
|
||||||
* Update actions/upload-artifact digest to ea165f8 [balena-renovate[bot]]
|
|
||||||
|
|
||||||
## 21.1.6 - 2025-04-03
|
|
||||||
|
|
||||||
* Update actions/setup-node digest to cdca736 [balena-renovate[bot]]
|
|
||||||
|
|
||||||
## 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]
|
|
||||||
|
|
||||||
## 20.2.2 - 2025-01-12
|
|
||||||
|
|
||||||
* Use the promises namespace of balena-image-fs [Thodoris Greasidis]
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary> Update balena-device-init to 8.1.3 & balena-image-fs to 7.3.0 [Thodoris Greasidis] </summary>
|
|
||||||
|
|
||||||
> ### balena-image-fs-7.3.0 - 2025-01-06
|
|
||||||
>
|
|
||||||
> * Drop Bluebird from devDependencies [Thodoris Greasidis]
|
|
||||||
> * flowzone: Remove empty with clause [Thodoris Greasidis]
|
|
||||||
> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
> ### balena-image-fs-7.2.2 - 2024-01-02
|
|
||||||
>
|
|
||||||
> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
|
|
||||||
>
|
|
||||||
> ### balena-image-fs-7.2.1 - 2023-12-19
|
|
||||||
>
|
|
||||||
> * Remove repo config from flowzone.yml [Kyle Harding]
|
|
||||||
>
|
|
||||||
> ### balena-image-fs-7.2.0 - 2023-01-20
|
|
||||||
>
|
|
||||||
> * Add support for Node 18 [Akis Kesoglou]
|
|
||||||
>
|
|
||||||
> ### balena-image-fs-7.1.2 - 2023-01-05
|
|
||||||
>
|
|
||||||
> * Update dependencies [ab77]
|
|
||||||
>
|
|
||||||
> ### balena-image-fs-7.1.1 - 2022-12-20
|
|
||||||
>
|
|
||||||
> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
|
|
||||||
>
|
|
||||||
> ### balena-image-fs-7.1.0 - 2022-12-13
|
|
||||||
>
|
|
||||||
> * update dependencies [Zane Hitchcox]
|
|
||||||
>
|
|
||||||
> ### balena-device-init-8.1.3 - 2025-01-09
|
|
||||||
>
|
|
||||||
> * README: Remove the travisci badge [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
> ### balena-device-init-8.1.2 - 2025-01-09
|
|
||||||
>
|
|
||||||
>
|
|
||||||
> <details>
|
|
||||||
> <summary> Use the promises namespace of balena-image-fs [Thodoris Greasidis] </summary>
|
|
||||||
>
|
|
||||||
>> #### balena-image-fs-7.3.0 - 2025-01-06
|
|
||||||
>>
|
|
||||||
>> * Drop Bluebird from devDependencies [Thodoris Greasidis]
|
|
||||||
>> * flowzone: Remove empty with clause [Thodoris Greasidis]
|
|
||||||
>> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
|
|
||||||
>>
|
|
||||||
>> #### balena-image-fs-7.2.2 - 2024-01-02
|
|
||||||
>>
|
|
||||||
>> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
|
|
||||||
>>
|
|
||||||
>> #### balena-image-fs-7.2.1 - 2023-12-19
|
|
||||||
>>
|
|
||||||
>> * Remove repo config from flowzone.yml [Kyle Harding]
|
|
||||||
>>
|
|
||||||
>> #### balena-image-fs-7.2.0 - 2023-01-20
|
|
||||||
>>
|
|
||||||
>> * Add support for Node 18 [Akis Kesoglou]
|
|
||||||
>>
|
|
||||||
>> #### balena-image-fs-7.1.2 - 2023-01-05
|
|
||||||
>>
|
|
||||||
>> * Update dependencies [ab77]
|
|
||||||
>>
|
|
||||||
>> #### balena-image-fs-7.1.1 - 2022-12-20
|
|
||||||
>>
|
|
||||||
>> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
|
|
||||||
>>
|
|
||||||
>> #### balena-image-fs-7.1.0 - 2022-12-13
|
|
||||||
>>
|
|
||||||
>> * update dependencies [Zane Hitchcox]
|
|
||||||
>>
|
|
||||||
>
|
|
||||||
> </details>
|
|
||||||
>
|
|
||||||
>
|
|
||||||
> ### balena-device-init-8.1.1 - 2025-01-06
|
|
||||||
>
|
|
||||||
> * Convert some parts to async await and simplify [Thodoris Greasidis]
|
|
||||||
> * Avoid unnecessary destructuring [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 20.2.1 - 2025-01-01
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary> Update balena-preload to 17.0.0 [Thodoris Greasidis] </summary>
|
|
||||||
|
|
||||||
> ### balena-preload-17.0.0 - 2024-10-21
|
|
||||||
>
|
|
||||||
> * Improve typings [Thodoris Greasidis]
|
|
||||||
> * Stop returning Bluebird promises & drop it from the dependencies [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 20.2.0 - 2024-12-31
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary> os configure: Give precedence to the boot partition located in the image over the device-type.json contents [Thodoris Greasidis] </summary>
|
|
||||||
|
|
||||||
> ### balena-device-init-8.1.0 - Invalid date
|
|
||||||
>
|
|
||||||
> * Try to find the boot partition by inspecting the image [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
> ### balena-device-init-8.0.1 - 2024-12-19
|
|
||||||
>
|
|
||||||
> * Drop the unnecessary eslint.config.js [Thodoris Greasidis]
|
|
||||||
> * packacke.json: Explicitly set type commonjs [Thodoris Greasidis]
|
|
||||||
>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 20.1.6 - 2024-12-30
|
## 20.1.6 - 2024-12-30
|
||||||
|
|
||||||
* Add more realistic os configure tests [Thodoris Greasidis]
|
* Add more realistic os configure tests [Thodoris Greasidis]
|
||||||
|
@ -14,7 +14,7 @@ The balena CLI is an open source project and your contribution is welcome!
|
|||||||
In order to ease development:
|
In order to ease development:
|
||||||
|
|
||||||
* `npm run build:fast` skips some of the build steps for interactive testing, or
|
* `npm run build:fast` skips some of the build steps for interactive testing, or
|
||||||
* `npm run test:source` skips testing the standalone packages (which is rather slow)
|
* `npm run test:source` skips testing the standalone zip packages (which is rather slow)
|
||||||
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
|
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
|
||||||
|
|
||||||
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is
|
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is
|
||||||
|
@ -8,8 +8,8 @@ There are 3 options to choose from to install balena's CLI:
|
|||||||
|
|
||||||
* [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the
|
* [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the
|
||||||
traditional graphical desktop application installers.
|
traditional graphical desktop application installers.
|
||||||
* [Standalone tar.gz Package](#standalone-targz-package): these are plain tar.gz files with the balena CLI
|
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
|
||||||
bundled within. Available for all platforms: Linux, Windows, macOS.
|
executable in them: extract and run. Available for all platforms: Linux, Windows, macOS.
|
||||||
Recommended also for scripted installation in CI (continuous integration) environments.
|
Recommended also for scripted installation in CI (continuous integration) environments.
|
||||||
* [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested
|
* [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested
|
||||||
in integrating the balena CLI in their existing projects or workflow.
|
in integrating the balena CLI in their existing projects or workflow.
|
||||||
@ -30,9 +30,9 @@ instructions:
|
|||||||
> If you would like to use WSL, follow the [installations instructions for
|
> If you would like to use WSL, follow the [installations instructions for
|
||||||
> Linux](./INSTALL-LINUX.md) rather than Windows, as WSL consists of a Linux environment.
|
> Linux](./INSTALL-LINUX.md) rather than Windows, as WSL consists of a Linux environment.
|
||||||
|
|
||||||
If you had previously installed the CLI using a standalone tar package, it may be a good idea to
|
If you had previously installed the CLI using a standalone zip package, it may be a good idea to
|
||||||
check your system's `PATH` environment variable for duplicate entries, as the terminal will use the
|
check your system's `PATH` environment variable for duplicate entries, as the terminal will use the
|
||||||
entry that comes first. Check the [Standalone tar.gz Package](#standalone-targz-package) instructions
|
entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package) instructions
|
||||||
for how to modify the PATH variable.
|
for how to modify the PATH variable.
|
||||||
|
|
||||||
By default, the CLI is installed to the following folders:
|
By default, the CLI is installed to the following folders:
|
||||||
@ -42,17 +42,18 @@ OS | Folders
|
|||||||
Windows: | `C:\Program Files\balena-cli\`
|
Windows: | `C:\Program Files\balena-cli\`
|
||||||
macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena`
|
macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena`
|
||||||
|
|
||||||
## Standalone tar.gz Package
|
## Standalone Zip Package
|
||||||
|
|
||||||
1. Download the latest tar.gz file from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||||
Look for a file name that ends with the word "standalone", for example:
|
Look for a file name that ends with the word "standalone", for example:
|
||||||
`balena-cli-vX.Y.Z-linux-x64-standalone.tar.gz` ← _also for the Windows Subsystem for Linux_
|
`balena-cli-vX.Y.Z-linux-x64-standalone.zip` ← _also for the Windows Subsystem for Linux_
|
||||||
`balena-cli-vX.Y.Z-macOS-x64-standalone.tar.gz`
|
`balena-cli-vX.Y.Z-macOS-x64-standalone.zip`
|
||||||
`balena-cli-vX.Y.Z-windows-x64-standalone.tar.gz`
|
`balena-cli-vX.Y.Z-windows-x64-standalone.zip`
|
||||||
|
|
||||||
2. Extract the tar.gz file contents to any folder you choose. The extracted contents will be a `balena` folder containing a `bin` subdirectory.
|
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||||
|
`balena-cli` folder.
|
||||||
|
|
||||||
3. Add the `balena/bin` folder to the system's `PATH` environment variable.
|
3. Add the `balena-cli` folder to the system's `PATH` environment variable.
|
||||||
See instructions for:
|
See instructions for:
|
||||||
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
|
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
|
||||||
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
|
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
|
||||||
@ -60,14 +61,14 @@ macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena`
|
|||||||
|
|
||||||
> * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and
|
> * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and
|
||||||
> workaround](https://github.com/balena-io/balena-cli/issues/2244).
|
> workaround](https://github.com/balena-io/balena-cli/issues/2244).
|
||||||
> * **Linux Alpine** and **Busybox:** the standalone tar.gz package is not currently compatible with
|
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with
|
||||||
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
|
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
|
||||||
> For these, consider the [NPM Installation](#npm-installation) option.
|
> For these, consider the [NPM Installation](#npm-installation) option.
|
||||||
> * Note that moving the `balena/bin/balena` executable out of the extracted `balena` folder on its own
|
> * Note that moving the `balena` executable out of the extracted `balena-cli` folder on its own
|
||||||
> (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other
|
> (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other
|
||||||
> folders and files also present in the `balena` folder.
|
> folders and files also present in the `balena-cli` folder.
|
||||||
|
|
||||||
To update the CLI to a new version, download a new release tar.gz file and replace the previous
|
To update the CLI to a new version, download a new release zip file and replace the previous
|
||||||
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
||||||
as described above.
|
as described above.
|
||||||
|
|
||||||
@ -77,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
|
The npm installation involves building native (platform-specific) binary modules, which require
|
||||||
some development tools to be installed first, as follows.
|
some development tools to be installed first, as follows.
|
||||||
|
|
||||||
> **The balena CLI currently requires Node.js version >=20.6.0**
|
> **The balena CLI currently requires Node.js version ^20.6.0**
|
||||||
> **Versions 23 and later are not yet fully supported.**
|
> **Versions 21 and later are not yet fully supported.**
|
||||||
|
|
||||||
### Install development tools
|
### Install development tools
|
||||||
|
|
||||||
@ -88,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++
|
$ 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
|
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||||
$ . ~/.bashrc
|
$ . ~/.bashrc
|
||||||
$ nvm install 22
|
$ nvm install 20
|
||||||
```
|
```
|
||||||
|
|
||||||
The `curl` command line above uses
|
The `curl` command line above uses
|
||||||
@ -105,7 +106,7 @@ recommended.
|
|||||||
```sh
|
```sh
|
||||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||||
$ . ~/.bashrc
|
$ . ~/.bashrc
|
||||||
$ nvm install 22
|
$ nvm install 20
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **Windows** (not WSL)
|
#### **Windows** (not WSL)
|
||||||
@ -113,7 +114,7 @@ $ nvm install 22
|
|||||||
Install:
|
Install:
|
||||||
|
|
||||||
* If you'd like the ability to switch between Node.js versions, install
|
* If you'd like the ability to switch between Node.js versions, install
|
||||||
- Node.js v22 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
- Node.js v20 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)
|
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||||
instead.
|
instead.
|
||||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||||
|
@ -7,8 +7,8 @@ Selected operating system: **macOS**
|
|||||||
|
|
||||||
1. Download the installer from the [latest release
|
1. Download the installer from the [latest release
|
||||||
page](https://github.com/balena-io/balena-cli/releases/latest).
|
page](https://github.com/balena-io/balena-cli/releases/latest).
|
||||||
Look for a file name that ends with "-installer.pkg":
|
Look for a file name that ends with "-installer.pkg":
|
||||||
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
||||||
|
|
||||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||||
instructions.
|
instructions.
|
||||||
|
@ -8,7 +8,7 @@ Selected operating system: **Windows**
|
|||||||
1. Download the installer from the [latest release
|
1. Download the installer from the [latest release
|
||||||
page](https://github.com/balena-io/balena-cli/releases/latest).
|
page](https://github.com/balena-io/balena-cli/releases/latest).
|
||||||
Look for a file name that ends with "-installer.exe":
|
Look for a file name that ends with "-installer.exe":
|
||||||
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
||||||
|
|
||||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||||
instructions.
|
instructions.
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
## Migrating to balena CLI v22
|
|
||||||
|
|
||||||
This guide outlines the changes introduced in balena CLI v22 and provides instructions for when and how to migrate.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### For Installer Users (Windows .exe, macOS .pkg)
|
|
||||||
|
|
||||||
If you are using the Windows executable (.exe) or macOS package (.pkg) installers, **no changes** are required for this update. You can continue to use the installers as before.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### For npm Installation Users
|
|
||||||
|
|
||||||
If you installed balena CLI via npm, **no changes** are required for this update. Your existing installation and update process remains the same.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### For Standalone Installation Users
|
|
||||||
|
|
||||||
Users of the standalone balena CLI will need to make the following adjustments:
|
|
||||||
|
|
||||||
1. **Archive Format Change**: The distribution archive format has changed from `.zip` to `.tar.gz`. You will need to use the `tar` command instead of `unzip` to extract the CLI.
|
|
||||||
|
|
||||||
* **Previous command (v21.x.x and older):**
|
|
||||||
```bash
|
|
||||||
unzip balena-cli-v21.1.12-linux-x64-standalone.zip
|
|
||||||
```
|
|
||||||
* **New command (v22.0.0 and newer):**
|
|
||||||
```bash
|
|
||||||
tar -xzf balena-cli-v22.0.0-linux-x64-standalone.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Executable Path Change**: The path to the balena CLI executable within the extracted folder has been updated.
|
|
||||||
|
|
||||||
* **Previous path (v21.x.x and older):**
|
|
||||||
```
|
|
||||||
balena-cli/balena
|
|
||||||
```
|
|
||||||
* **New path (v22.0.0 and newer):**
|
|
||||||
```
|
|
||||||
balena/bin/balena
|
|
||||||
```
|
|
||||||
|
|
||||||
Please update your scripts and any aliases to reflect these changes if you are using the standalone version.
|
|
@ -20,8 +20,6 @@ GitHub](https://github.com/balena-io/balena-cli/), and your contribution is also
|
|||||||
Check the [balena CLI installation instructions on
|
Check the [balena CLI installation instructions on
|
||||||
GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||||
|
|
||||||
Note for v22 Migration: If you are upgrading to balena CLI v22 from a previous standalone installation, please [see the v22 Migration Guide](https://github.com/balena-io/balena-cli/blob/master/MIGRATIONS.md) for installation changes.
|
|
||||||
|
|
||||||
## Choosing a shell (command prompt/terminal)
|
## Choosing a shell (command prompt/terminal)
|
||||||
|
|
||||||
On **Windows,** the standard Command Prompt (`cmd.exe`) and
|
On **Windows,** the standard Command Prompt (`cmd.exe`) and
|
||||||
|
@ -115,7 +115,7 @@ If nothing seems to help, consider also using a different client-side terminal a
|
|||||||
## "Docker seems to be unavailable" error when using Windows Subsystem for Linux (WSL)
|
## "Docker seems to be unavailable" error when using Windows Subsystem for Linux (WSL)
|
||||||
|
|
||||||
When running on WSL, the recommendation is to install a CLI release for Linux, like the standalone
|
When running on WSL, the recommendation is to install a CLI release for Linux, like the standalone
|
||||||
tar.gz package for Linux. However, commands like "balena build" will, by default, attempt to reach the
|
zip package for Linux. However, commands like "balena build" will, by default, attempt to reach the
|
||||||
Docker daemon at the Unix socket path `/var/run/docker.sock`, while Docker Desktop for Windows uses
|
Docker daemon at the Unix socket path `/var/run/docker.sock`, while Docker Desktop for Windows uses
|
||||||
a Windows named pipe at `//./pipe/docker_engine` (which the Linux CLI on WSL cannot use). A
|
a Windows named pipe at `//./pipe/docker_engine` (which the Linux CLI on WSL cannot use). A
|
||||||
solution is:
|
solution is:
|
||||||
|
@ -75,8 +75,6 @@ const renamedOclifStandalone: PathByPlatform = {
|
|||||||
|
|
||||||
export async function signFilesForNotarization() {
|
export async function signFilesForNotarization() {
|
||||||
console.log('Signing files for notarization');
|
console.log('Signing files for notarization');
|
||||||
// If signFilesForNotarization is called on the test CI environment (which will not set CSC_LINK)
|
|
||||||
// then we skip the signing process.
|
|
||||||
if (process.platform !== 'darwin' || !process.env.CSC_LINK) {
|
if (process.platform !== 'darwin' || !process.env.CSC_LINK) {
|
||||||
console.log('Skipping signing for notarization');
|
console.log('Skipping signing for notarization');
|
||||||
return;
|
return;
|
||||||
@ -225,7 +223,7 @@ async function renameStandalone() {
|
|||||||
*/
|
*/
|
||||||
async function signWindowsInstaller() {
|
async function signWindowsInstaller() {
|
||||||
if (process.env.SM_CODE_SIGNING_CERT_SHA1_HASH) {
|
if (process.env.SM_CODE_SIGNING_CERT_SHA1_HASH) {
|
||||||
const exeName = (await getOclifInstallersOriginalNames())[process.platform];
|
const exeName = renamedOclifInstallers[process.platform];
|
||||||
console.log(`Signing installer "${exeName}"`);
|
console.log(`Signing installer "${exeName}"`);
|
||||||
// trust ...
|
// trust ...
|
||||||
await execFileAsync('signtool.exe', [
|
await execFileAsync('signtool.exe', [
|
||||||
@ -259,14 +257,12 @@ async function notarizeMacInstaller(): Promise<void> {
|
|||||||
const appleId =
|
const appleId =
|
||||||
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
|
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
|
||||||
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
|
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
|
||||||
const appPath = (await getOclifInstallersOriginalNames())[process.platform];
|
|
||||||
console.log(`Notarizing file "${appPath}"`);
|
|
||||||
|
|
||||||
if (appleIdPassword && teamId) {
|
if (appleIdPassword && teamId) {
|
||||||
await notarize({
|
await notarize({
|
||||||
tool: 'notarytool',
|
tool: 'notarytool',
|
||||||
teamId,
|
teamId,
|
||||||
appPath,
|
appPath: renamedOclifInstallers.darwin,
|
||||||
appleId,
|
appleId,
|
||||||
appleIdPassword,
|
appleIdPassword,
|
||||||
});
|
});
|
||||||
@ -306,6 +302,7 @@ export async function buildOclifInstaller() {
|
|||||||
console.log('=======================================================');
|
console.log('=======================================================');
|
||||||
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
|
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
|
||||||
await oclifRun([packCmd].concat(...packOpts), oclifPath);
|
await oclifRun([packCmd].concat(...packOpts), oclifPath);
|
||||||
|
await renameInstallers();
|
||||||
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
||||||
// The macOS installer is automatically signed by oclif (which runs the
|
// The macOS installer is automatically signed by oclif (which runs the
|
||||||
// `pkgbuild` tool), using the certificate name given in package.json
|
// `pkgbuild` tool), using the certificate name given in package.json
|
||||||
@ -317,7 +314,6 @@ export async function buildOclifInstaller() {
|
|||||||
await notarizeMacInstaller(); // Notarize
|
await notarizeMacInstaller(); // Notarize
|
||||||
console.log('Package notarized.');
|
console.log('Package notarized.');
|
||||||
}
|
}
|
||||||
await renameInstallers();
|
|
||||||
console.log(`oclif installer build completed`);
|
console.log(`oclif installer build completed`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -353,5 +349,4 @@ export async function testShrinkwrap(): Promise<void> {
|
|||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
await whichSpawn(path.resolve(__dirname, 'test-lock-deduplicated.sh'));
|
await whichSpawn(path.resolve(__dirname, 'test-lock-deduplicated.sh'));
|
||||||
}
|
}
|
||||||
await Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,6 @@ GitHub](https://github.com/balena-io/balena-cli/), and your contribution is also
|
|||||||
Check the [balena CLI installation instructions on
|
Check the [balena CLI installation instructions on
|
||||||
GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||||
|
|
||||||
Note for v22 Migration: If you are upgrading to balena CLI v22 from a previous standalone installation, please [see the v22 Migration Guide](https://github.com/balena-io/balena-cli/blob/master/MIGRATIONS.md) for installation changes.
|
|
||||||
|
|
||||||
## Choosing a shell (command prompt/terminal)
|
## Choosing a shell (command prompt/terminal)
|
||||||
|
|
||||||
On **Windows,** the standard Command Prompt (`cmd.exe`) and
|
On **Windows,** the standard Command Prompt (`cmd.exe`) and
|
||||||
@ -328,8 +326,6 @@ or to authenticate requests to the API with an 'Authorization: Bearer <key>' hea
|
|||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
$ balena api-key generate "Jenkins Key"
|
$ balena api-key generate "Jenkins Key"
|
||||||
$ balena api-key generate "Jenkins Key" 2025-10-30
|
|
||||||
$ balena api-key generate "Jenkins Key" never
|
|
||||||
|
|
||||||
### Arguments
|
### Arguments
|
||||||
|
|
||||||
@ -337,10 +333,6 @@ Examples:
|
|||||||
|
|
||||||
the API key name
|
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
|
## api-key list
|
||||||
|
|
||||||
### Aliases
|
### Aliases
|
||||||
@ -576,9 +568,9 @@ Generate a config.json file for a device or fleet.
|
|||||||
The target balenaOS version must be specified with the --version option.
|
The target balenaOS version must be specified with the --version option.
|
||||||
|
|
||||||
The '--dev' option is used to configure balenaOS to operate in development mode,
|
The '--dev' option is used to configure balenaOS to operate in development mode,
|
||||||
allowing unauthenticated root ssh access and exposing network ports such as
|
allowing anauthenticated root ssh access and exposing network ports such as
|
||||||
balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true`
|
balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true`
|
||||||
to be inserted in the 'config.json' file in the image's boot partition. Development
|
to be inserted in the 'config.json' file in the image's boot partion. Development
|
||||||
mode (as a configurable option) is applicable to balenaOS releases from early
|
mode (as a configurable option) is applicable to balenaOS releases from early
|
||||||
2022. Older releases have separate development and production balenaOS images
|
2022. Older releases have separate development and production balenaOS images
|
||||||
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
|
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
|
||||||
@ -2874,9 +2866,9 @@ The --device-type option is used to override the fleet's default device type,
|
|||||||
in case of a fleet with mixed device types.
|
in case of a fleet with mixed device types.
|
||||||
|
|
||||||
The '--dev' option is used to configure balenaOS to operate in development mode,
|
The '--dev' option is used to configure balenaOS to operate in development mode,
|
||||||
allowing unauthenticated root ssh access and exposing network ports such as
|
allowing anauthenticated root ssh access and exposing network ports such as
|
||||||
balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true`
|
balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true`
|
||||||
to be inserted in the 'config.json' file in the image's boot partition. Development
|
to be inserted in the 'config.json' file in the image's boot partion. Development
|
||||||
mode (as a configurable option) is applicable to balenaOS releases from early
|
mode (as a configurable option) is applicable to balenaOS releases from early
|
||||||
2022. Older releases have separate development and production balenaOS images
|
2022. Older releases have separate development and production balenaOS images
|
||||||
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
|
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
|
||||||
|
3245
npm-shrinkwrap.json
generated
3245
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "balena-cli",
|
"name": "balena-cli",
|
||||||
"version": "22.1.1",
|
"version": "20.1.6",
|
||||||
"description": "The official balena Command Line Interface",
|
"description": "The official balena Command Line Interface",
|
||||||
"main": "./build/app.js",
|
"main": "./build/app.js",
|
||||||
"homepage": "https://github.com/balena-io/balena-cli",
|
"homepage": "https://github.com/balena-io/balena-cli",
|
||||||
@ -38,7 +38,6 @@
|
|||||||
"build:completion": "node completion/generate-completion.js",
|
"build:completion": "node completion/generate-completion.js",
|
||||||
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
|
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
|
||||||
"build:installer": "ts-node --transpile-only automation/run.ts build:installer",
|
"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",
|
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
|
||||||
"pretest": "npm run build",
|
"pretest": "npm run build",
|
||||||
"test": "npm run test:shrinkwrap && npm run test:core",
|
"test": "npm run test:shrinkwrap && npm run test:core",
|
||||||
@ -72,7 +71,7 @@
|
|||||||
"author": "Balena Inc. (https://balena.io/)",
|
"author": "Balena Inc. (https://balena.io/)",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.6.0 <23"
|
"node": "^20.6.0"
|
||||||
},
|
},
|
||||||
"oclif": {
|
"oclif": {
|
||||||
"bin": "balena",
|
"bin": "balena",
|
||||||
@ -124,6 +123,7 @@
|
|||||||
"@types/node-cleanup": "^2.1.2",
|
"@types/node-cleanup": "^2.1.2",
|
||||||
"@types/prettyjson": "^0.0.33",
|
"@types/prettyjson": "^0.0.33",
|
||||||
"@types/progress-stream": "^2.0.2",
|
"@types/progress-stream": "^2.0.2",
|
||||||
|
"@types/request": "^2.48.7",
|
||||||
"@types/rewire": "^2.5.30",
|
"@types/rewire": "^2.5.30",
|
||||||
"@types/rimraf": "^3.0.2",
|
"@types/rimraf": "^3.0.2",
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
@ -153,28 +153,28 @@
|
|||||||
"mocha": "^10.6.0",
|
"mocha": "^10.6.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"mock-require": "^3.0.3",
|
"mock-require": "^3.0.3",
|
||||||
"nock": "^14.0.4",
|
"nock": "^13.2.1",
|
||||||
"oclif": "^4.17.0",
|
"oclif": "^4.17.0",
|
||||||
"rewire": "^7.0.0",
|
"rewire": "^7.0.0",
|
||||||
"simple-git": "^3.14.1",
|
"simple-git": "^3.14.1",
|
||||||
"sinon": "^19.0.0",
|
"sinon": "^19.0.0",
|
||||||
"string-to-stream": "^3.0.1",
|
"string-to-stream": "^3.0.1",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.4.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@balena/compose": "^7.0.9",
|
"@balena/compose": "^6.0.0",
|
||||||
"@balena/dockerignore": "^1.0.2",
|
"@balena/dockerignore": "^1.0.2",
|
||||||
"@balena/env-parsing": "^1.1.8",
|
"@balena/env-parsing": "^1.1.8",
|
||||||
"@balena/es-version": "^1.0.1",
|
"@balena/es-version": "^1.0.1",
|
||||||
"@oclif/core": "^4.1.0",
|
"@oclif/core": "^4.1.0",
|
||||||
"@sentry/node": "^9.0.0",
|
"@sentry/node": "^6.16.1",
|
||||||
"balena-config-json": "^4.2.7",
|
"balena-config-json": "^4.2.0",
|
||||||
"balena-device-init": "^8.1.11",
|
"balena-device-init": "^8.0.0",
|
||||||
"balena-errors": "^4.7.3",
|
"balena-errors": "^4.7.3",
|
||||||
"balena-image-fs": "^7.5.2",
|
"balena-image-fs": "^7.0.6",
|
||||||
"balena-preload": "^18.0.4",
|
"balena-preload": "^16.0.0",
|
||||||
"balena-sdk": "^21.3.0",
|
"balena-sdk": "^20.8.0",
|
||||||
"balena-semver": "^2.3.0",
|
"balena-semver": "^2.3.0",
|
||||||
"balena-settings-client": "^5.0.2",
|
"balena-settings-client": "^5.0.2",
|
||||||
"balena-settings-storage": "^8.1.0",
|
"balena-settings-storage": "^8.1.0",
|
||||||
@ -185,13 +185,12 @@
|
|||||||
"cli-truncate": "^2.1.0",
|
"cli-truncate": "^2.1.0",
|
||||||
"color-hash": "^1.1.1",
|
"color-hash": "^1.1.1",
|
||||||
"common-tags": "^1.7.2",
|
"common-tags": "^1.7.2",
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"denymount": "^2.3.0",
|
"denymount": "^2.3.0",
|
||||||
"docker-modem": "^5.0.6",
|
"docker-modem": "^5.0.3",
|
||||||
"docker-progress": "^5.1.3",
|
"docker-progress": "^5.1.3",
|
||||||
"dockerode": "^4.0.5",
|
"dockerode": "^4.0.2",
|
||||||
"ejs": "^3.1.6",
|
"ejs": "^3.1.6",
|
||||||
"etcher-sdk": "^10.0.0",
|
"etcher-sdk": "9.1.0",
|
||||||
"express": "^4.17.2",
|
"express": "^4.17.2",
|
||||||
"fast-boot2": "^1.1.0",
|
"fast-boot2": "^1.1.0",
|
||||||
"fast-levenshtein": "^3.0.0",
|
"fast-levenshtein": "^3.0.0",
|
||||||
@ -219,8 +218,9 @@
|
|||||||
"prettyjson": "^1.2.5",
|
"prettyjson": "^1.2.5",
|
||||||
"progress-stream": "^2.0.0",
|
"progress-stream": "^2.0.0",
|
||||||
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
|
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
|
||||||
"resin-cli-form": "^4.0.0",
|
"request": "^2.88.2",
|
||||||
"resin-cli-visuals": "^3.0.0",
|
"resin-cli-form": "^3.0.0",
|
||||||
|
"resin-cli-visuals": "^2.0.1",
|
||||||
"resin-doodles": "^0.2.0",
|
"resin-doodles": "^0.2.0",
|
||||||
"resin-stream-logger": "^0.1.2",
|
"resin-stream-logger": "^0.1.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
@ -248,6 +248,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"versionist": {
|
"versionist": {
|
||||||
"publishedAt": "2025-06-19T09:32:53.877Z"
|
"publishedAt": "2024-12-30T17:16:10.325Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
repo.yml
2
repo.yml
@ -6,8 +6,6 @@ upstream:
|
|||||||
url: 'https://github.com/balena-io/balena-sdk'
|
url: 'https://github.com/balena-io/balena-sdk'
|
||||||
- repo: 'balena-config-json'
|
- repo: 'balena-config-json'
|
||||||
url: 'https://github.com/balena-io-modules/balena-config-json'
|
url: 'https://github.com/balena-io-modules/balena-config-json'
|
||||||
- repo: 'balena-image-fs'
|
|
||||||
url: 'https://github.com/balena-io-modules/balena-image-fs'
|
|
||||||
- repo: 'balena-device-init'
|
- repo: 'balena-device-init'
|
||||||
url: 'https://github.com/balena-io-modules/balena-device-init'
|
url: 'https://github.com/balena-io-modules/balena-device-init'
|
||||||
- repo: 'balena-image-manager'
|
- repo: 'balena-image-manager'
|
||||||
|
12
src/app.ts
12
src/app.ts
@ -34,14 +34,18 @@ export const setupSentry = onceAsync(async () => {
|
|||||||
const config = await import('./config');
|
const config = await import('./config');
|
||||||
const Sentry = await import('@sentry/node');
|
const Sentry = await import('@sentry/node');
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
autoSessionTracking: false,
|
||||||
dsn: config.sentryDsn,
|
dsn: config.sentryDsn,
|
||||||
release: packageJSON.version,
|
release: packageJSON.version,
|
||||||
});
|
});
|
||||||
Sentry.getCurrentScope().setExtras({
|
Sentry.configureScope((scope) => {
|
||||||
is_pkg: !!(process as any).pkg,
|
scope.setExtras({
|
||||||
node_version: process.version,
|
is_pkg: !!(process as any).pkg,
|
||||||
platform: process.platform,
|
node_version: process.version,
|
||||||
|
platform: process.platform,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
return Sentry.getCurrentHub();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function checkNodeVersion() {
|
async function checkNodeVersion() {
|
||||||
|
@ -17,16 +17,7 @@
|
|||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { Args, Command } from '@oclif/core';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
|
||||||
formatDuration,
|
|
||||||
intervalToDuration,
|
|
||||||
isValid,
|
|
||||||
parseISO,
|
|
||||||
} from 'date-fns';
|
|
||||||
|
|
||||||
// In days
|
|
||||||
const durations = [1, 7, 30, 90];
|
|
||||||
|
|
||||||
async function isLoggedInWithJwt() {
|
async function isLoggedInWithJwt() {
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
@ -50,21 +41,13 @@ export default class GenerateCmd extends Command {
|
|||||||
This key can be used to log into the CLI using 'balena login --token <key>',
|
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.
|
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||||
`;
|
`;
|
||||||
public static examples = [
|
public static examples = ['$ balena api-key generate "Jenkins Key"'];
|
||||||
'$ 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 = {
|
public static args = {
|
||||||
name: Args.string({
|
name: Args.string({
|
||||||
description: 'the API key name',
|
description: 'the API key name',
|
||||||
required: true,
|
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;
|
public static authenticated = true;
|
||||||
@ -72,61 +55,9 @@ export default class GenerateCmd extends Command {
|
|||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(GenerateCmd);
|
const { args: params } = await this.parse(GenerateCmd);
|
||||||
|
|
||||||
let expiryDateResponse: string | number | undefined = params.expiryDate;
|
|
||||||
let key;
|
let key;
|
||||||
try {
|
try {
|
||||||
if (!expiryDateResponse) {
|
key = await getBalenaSdk().models.apiKey.create(params.name);
|
||||||
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) {
|
} catch (e) {
|
||||||
if (e.name === 'BalenaNotLoggedIn') {
|
if (e.name === 'BalenaNotLoggedIn') {
|
||||||
if (await isLoggedInWithJwt()) {
|
if (await isLoggedInWithJwt()) {
|
||||||
|
@ -64,12 +64,7 @@ export default class ConfigInjectCmd extends Command {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
await config.write(
|
await config.write(drive, '', configJSON);
|
||||||
drive,
|
|
||||||
// Will be removed in the next major of balena-config-json
|
|
||||||
undefined,
|
|
||||||
configJSON,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.info('Done');
|
console.info('Done');
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export default class ConfigReadCmd extends Command {
|
|||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
const configJSON = await config.read(drive);
|
const configJSON = await config.read(drive, '');
|
||||||
|
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
console.log(JSON.stringify(configJSON, null, 4));
|
console.log(JSON.stringify(configJSON, null, 4));
|
||||||
|
@ -62,7 +62,7 @@ export default class ConfigReconfigureCmd extends Command {
|
|||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
const { uuid } = await config.read(drive);
|
const { uuid } = await config.read(drive, '');
|
||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
if (!uuid) {
|
if (!uuid) {
|
||||||
|
@ -64,19 +64,14 @@ export default class ConfigWriteCmd extends Command {
|
|||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
const configJSON = await config.read(drive);
|
const configJSON = await config.read(drive, '');
|
||||||
|
|
||||||
console.info(`Setting ${params.key} to ${params.value}`);
|
console.info(`Setting ${params.key} to ${params.value}`);
|
||||||
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
|
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
|
||||||
|
|
||||||
await denyMount(drive, async () => {
|
await denyMount(drive, async () => {
|
||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
await config.write(
|
await config.write(drive, '', configJSON);
|
||||||
drive,
|
|
||||||
// Will be removed in the next major of balena-config-json
|
|
||||||
undefined,
|
|
||||||
configJSON,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.info('Done');
|
console.info('Done');
|
||||||
|
@ -368,7 +368,6 @@ ${dockerignoreHelp}
|
|||||||
!opts.shouldUploadLogs,
|
!opts.shouldUploadLogs,
|
||||||
composeOpts.projectPath,
|
composeOpts.projectPath,
|
||||||
opts.createAsDraft,
|
opts.createAsDraft,
|
||||||
project.descriptors,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,59 +77,45 @@ export default class DeviceCmd extends Command {
|
|||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
let device: ExtendedDevice;
|
const device = (await balena.models.device.get(
|
||||||
if (options.json) {
|
params.uuid,
|
||||||
const [deviceBase, deviceComputed] = await Promise.all([
|
options.json
|
||||||
balena.models.device.get(params.uuid, {
|
? {
|
||||||
$expand: {
|
$expand: {
|
||||||
device_tag: {
|
device_tag: {
|
||||||
$select: ['tag_key', 'value'],
|
$select: ['tag_key', 'value'],
|
||||||
|
},
|
||||||
|
...expandForAppName.$expand,
|
||||||
},
|
},
|
||||||
...expandForAppName.$expand,
|
}
|
||||||
|
: {
|
||||||
|
$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;
|
||||||
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) {
|
if (options.view) {
|
||||||
const open = await import('open');
|
const open = await import('open');
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { Args, Command } from '@oclif/core';
|
||||||
|
import { promisify } from 'util';
|
||||||
import { stripIndent } from '../../utils/lazy';
|
import { stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
export default class LocalConfigureCmd extends Command {
|
export default class LocalConfigureCmd extends Command {
|
||||||
@ -236,7 +237,7 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
const bootPartition = await getBootPartition(target);
|
const bootPartition = await getBootPartition(target);
|
||||||
|
|
||||||
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
|
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||||
return await _fs.promises.readdir(this.CONNECTIONS_FOLDER);
|
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
|
||||||
});
|
});
|
||||||
|
|
||||||
let connectionFileName;
|
let connectionFileName;
|
||||||
@ -245,14 +246,13 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
} else if (_.includes(files, 'resin-sample.ignore')) {
|
} else if (_.includes(files, 'resin-sample.ignore')) {
|
||||||
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||||
await imagefs.interact(target, bootPartition, async (_fs) => {
|
await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||||
const contents = await _fs.promises.readFile(
|
const readFileAsync = promisify(_fs.readFile);
|
||||||
|
const writeFileAsync = promisify(_fs.writeFile);
|
||||||
|
const contents = await readFileAsync(
|
||||||
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
||||||
{ encoding: 'utf8' },
|
{ encoding: 'utf8' },
|
||||||
);
|
);
|
||||||
await _fs.promises.writeFile(
|
await writeFileAsync(`${this.CONNECTIONS_FOLDER}/resin-wifi`, contents);
|
||||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
|
||||||
contents,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
} else if (_.includes(files, 'resin-sample')) {
|
} else if (_.includes(files, 'resin-sample')) {
|
||||||
// Legacy mode, to be removed later
|
// Legacy mode, to be removed later
|
||||||
@ -266,7 +266,7 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
} else {
|
} else {
|
||||||
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
||||||
await imagefs.interact(target, bootPartition, async (_fs) => {
|
await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||||
await _fs.promises.writeFile(
|
await promisify(_fs.writeFile)(
|
||||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||||
this.CONNECTION_FILE,
|
this.CONNECTION_FILE,
|
||||||
);
|
);
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
import { Flags, Args, Command } from '@oclif/core';
|
import { Flags, Args, Command } from '@oclif/core';
|
||||||
import type { Interfaces } from '@oclif/core';
|
import type { Interfaces } from '@oclif/core';
|
||||||
import type * as BalenaSdk from 'balena-sdk';
|
import type * as BalenaSdk from 'balena-sdk';
|
||||||
|
import { promisify } from 'util';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
@ -291,7 +292,7 @@ export default class OsConfigureCmd extends Command {
|
|||||||
|
|
||||||
for (const { name, content } of files) {
|
for (const { name, content } of files) {
|
||||||
await imagefs.interact(image, bootPartition, async (_fs) => {
|
await imagefs.interact(image, bootPartition, async (_fs) => {
|
||||||
await _fs.promises.writeFile(
|
await promisify(_fs.writeFile)(
|
||||||
path.join(CONNECTIONS_FOLDER, name),
|
path.join(CONNECTIONS_FOLDER, name),
|
||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
|
@ -37,7 +37,6 @@ import type {
|
|||||||
Release,
|
Release,
|
||||||
} from 'balena-sdk';
|
} from 'balena-sdk';
|
||||||
import type { Preloader } from 'balena-preload';
|
import type { Preloader } from 'balena-preload';
|
||||||
import type * as Fs from 'fs';
|
|
||||||
|
|
||||||
export default class PreloadCmd extends Command {
|
export default class PreloadCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -162,42 +161,6 @@ 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
|
// balena-preload currently does not work with numerical app IDs
|
||||||
// Load app here, and use app slug from hereon
|
// Load app here, and use app slug from hereon
|
||||||
const fleetSlug: string | undefined = options.fleet
|
const fleetSlug: string | undefined = options.fleet
|
||||||
@ -332,7 +295,7 @@ Can be repeated to add multiple certificates.\
|
|||||||
owns__release: {
|
owns__release: {
|
||||||
$select: ['id', 'commit', 'end_timestamp', 'composition'],
|
$select: ['id', 'commit', 'end_timestamp', 'composition'],
|
||||||
$expand: {
|
$expand: {
|
||||||
release_image: {
|
contains__image: {
|
||||||
$select: ['image'],
|
$select: ['image'],
|
||||||
$expand: {
|
$expand: {
|
||||||
image: {
|
image: {
|
||||||
|
@ -38,11 +38,11 @@ import { stripIndent } from './utils/lazy';
|
|||||||
export async function trackCommand(commandSignature: string) {
|
export async function trackCommand(commandSignature: string) {
|
||||||
try {
|
try {
|
||||||
let Sentry: typeof import('@sentry/node');
|
let Sentry: typeof import('@sentry/node');
|
||||||
let scope: import('@sentry/node').Scope;
|
|
||||||
if (!process.env.BALENARC_NO_SENTRY) {
|
if (!process.env.BALENARC_NO_SENTRY) {
|
||||||
Sentry = await import('@sentry/node');
|
Sentry = await import('@sentry/node');
|
||||||
scope = Sentry.getCurrentScope();
|
Sentry.configureScope((scope) => {
|
||||||
scope.setExtra('command', commandSignature);
|
scope.setExtra('command', commandSignature);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const { getCachedUsername } = await import('./utils/bootstrap');
|
const { getCachedUsername } = await import('./utils/bootstrap');
|
||||||
let username: string | undefined;
|
let username: string | undefined;
|
||||||
@ -52,9 +52,11 @@ export async function trackCommand(commandSignature: string) {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
if (!process.env.BALENARC_NO_SENTRY) {
|
if (!process.env.BALENARC_NO_SENTRY) {
|
||||||
scope!.setUser({
|
Sentry!.configureScope((scope) => {
|
||||||
id: username,
|
scope.setUser({
|
||||||
username,
|
id: username,
|
||||||
|
username,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
@ -128,7 +128,6 @@ export const createRelease = async function (
|
|||||||
draft: boolean,
|
draft: boolean,
|
||||||
semver: string | undefined,
|
semver: string | undefined,
|
||||||
contract: import('@balena/compose/dist/release/models').ReleaseModel['contract'],
|
contract: import('@balena/compose/dist/release/models').ReleaseModel['contract'],
|
||||||
imgDescriptors: ImageDescriptor[],
|
|
||||||
): Promise<Release> {
|
): Promise<Release> {
|
||||||
const _ = require('lodash') as typeof import('lodash');
|
const _ = require('lodash') as typeof import('lodash');
|
||||||
const crypto = require('crypto') as typeof import('crypto');
|
const crypto = require('crypto') as typeof import('crypto');
|
||||||
@ -168,7 +167,6 @@ export const createRelease = async function (
|
|||||||
semver,
|
semver,
|
||||||
is_final: !draft,
|
is_final: !draft,
|
||||||
contract,
|
contract,
|
||||||
imgDescriptors,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -242,7 +240,7 @@ export const getPreviousRepos = (
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
},
|
},
|
||||||
$expand: {
|
$expand: {
|
||||||
release_image: {
|
contains__image: {
|
||||||
$select: 'image',
|
$select: 'image',
|
||||||
$expand: { image: { $select: 'is_stored_at__image_location' } },
|
$expand: { image: { $select: 'is_stored_at__image_location' } },
|
||||||
},
|
},
|
||||||
@ -254,7 +252,7 @@ export const getPreviousRepos = (
|
|||||||
.then(function (release) {
|
.then(function (release) {
|
||||||
// grab all images from the latest release, return all image locations in the registry
|
// grab all images from the latest release, return all image locations in the registry
|
||||||
if (release.length > 0) {
|
if (release.length > 0) {
|
||||||
const images = release[0].release_image as Array<{
|
const images = release[0].contains__image as Array<{
|
||||||
image: [SDK.Image];
|
image: [SDK.Image];
|
||||||
}>;
|
}>;
|
||||||
const { getRegistryAndName } =
|
const { getRegistryAndName } =
|
||||||
|
@ -1322,9 +1322,6 @@ async function pushAndUpdateServiceImages(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error messages are limited to 300KB characters in the API, so we truncate longer ones.
|
|
||||||
const MAX_ERROR_MESSAGE_LENGTH = 300_000;
|
|
||||||
|
|
||||||
async function pushServiceImages(
|
async function pushServiceImages(
|
||||||
docker: Dockerode,
|
docker: Dockerode,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
@ -1347,34 +1344,23 @@ async function pushServiceImages(
|
|||||||
delete serviceImage.build_log;
|
delete serviceImage.build_log;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are the only update-able image fields in bC atm, and passing
|
await releaseMod.updateImage(
|
||||||
// the whole image object in v7+ would result the allowlist to reject the request.
|
pineClient,
|
||||||
const imagePayload = _.pick(serviceImage, [
|
serviceImage.id,
|
||||||
'end_timestamp',
|
// These are the only update-able image fields in bC atm, and passing
|
||||||
'project_type',
|
// the whole image object in v7+ would result the allowlist to reject the request.
|
||||||
'error_message',
|
_.pick(serviceImage, [
|
||||||
'build_log',
|
'end_timestamp',
|
||||||
'push_timestamp',
|
'project_type',
|
||||||
'status',
|
'error_message',
|
||||||
'content_hash',
|
'build_log',
|
||||||
'dockerfile',
|
'push_timestamp',
|
||||||
'image_size',
|
'status',
|
||||||
]);
|
'content_hash',
|
||||||
|
'dockerfile',
|
||||||
if (
|
'image_size',
|
||||||
typeof imagePayload.error_message === 'string' &&
|
]),
|
||||||
imagePayload.error_message.length > MAX_ERROR_MESSAGE_LENGTH
|
);
|
||||||
) {
|
|
||||||
logger.logDebug(
|
|
||||||
`Truncating error message of image ${serviceImage.is_stored_at__image_location} to ${MAX_ERROR_MESSAGE_LENGTH} characters.`,
|
|
||||||
);
|
|
||||||
imagePayload.error_message = imagePayload.error_message.substring(
|
|
||||||
0,
|
|
||||||
MAX_ERROR_MESSAGE_LENGTH,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await releaseMod.updateImage(pineClient, serviceImage.id, imagePayload);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1389,7 +1375,6 @@ export async function deployProject(
|
|||||||
skipLogUpload: boolean,
|
skipLogUpload: boolean,
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
isDraft: boolean,
|
isDraft: boolean,
|
||||||
imgDescriptors: ImageDescriptor[],
|
|
||||||
): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
|
): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
|
||||||
const releaseMod = await import('@balena/compose/dist/release');
|
const releaseMod = await import('@balena/compose/dist/release');
|
||||||
const { createRelease, tagServiceImages } = await import('./compose');
|
const { createRelease, tagServiceImages } = await import('./compose');
|
||||||
@ -1420,7 +1405,6 @@ export async function deployProject(
|
|||||||
isDraft,
|
isDraft,
|
||||||
contract?.version,
|
contract?.version,
|
||||||
contract,
|
contract,
|
||||||
imgDescriptors,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const { client: pineClient, release, serviceImages } = $release;
|
const { client: pineClient, release, serviceImages } = $release;
|
||||||
@ -1610,9 +1594,7 @@ function buildProgressAdapter(inline: boolean) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to keep the regex match instead of startsWith as it also works with buffers
|
if (!str.startsWith('Successfully tagged ')) {
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
|
|
||||||
if (!/^Successfully tagged /.test(str)) {
|
|
||||||
const match = stepRegex.exec(str);
|
const match = stepRegex.exec(str);
|
||||||
if (match) {
|
if (match) {
|
||||||
step = match[1];
|
step = match[1];
|
||||||
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import type * as BalenaSdk from 'balena-sdk';
|
import type * as BalenaSdk from 'balena-sdk';
|
||||||
|
import * as semver from 'balena-semver';
|
||||||
import { getBalenaSdk, stripIndent } from './lazy';
|
import { getBalenaSdk, stripIndent } from './lazy';
|
||||||
|
|
||||||
export interface ImgConfig {
|
export interface ImgConfig {
|
||||||
@ -121,10 +122,16 @@ export function generateDeviceConfig(
|
|||||||
// os.getConfig always returns a config for an app
|
// os.getConfig always returns a config for an app
|
||||||
delete config.apiKey;
|
delete config.apiKey;
|
||||||
|
|
||||||
config.deviceApiKey =
|
if (deviceApiKey == null && semver.satisfies(options.version, '<2.0.3')) {
|
||||||
typeof deviceApiKey === 'string' && deviceApiKey
|
config.apiKey = await sdk.models.application.generateApiKey(
|
||||||
? deviceApiKey
|
application.id,
|
||||||
: await sdk.models.device.generateDeviceKey(device.uuid);
|
);
|
||||||
|
} else {
|
||||||
|
config.deviceApiKey =
|
||||||
|
typeof deviceApiKey === 'string' && deviceApiKey
|
||||||
|
? deviceApiKey
|
||||||
|
: await sdk.models.device.generateDeviceKey(device.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
})
|
})
|
||||||
|
@ -19,7 +19,7 @@ import { getVisuals } from './lazy';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import type * as Dockerode from 'dockerode';
|
import type * as Dockerode from 'dockerode';
|
||||||
import type Logger = require('./logger');
|
import type Logger = require('./logger');
|
||||||
import type got from 'got';
|
import type { Request } from 'request';
|
||||||
|
|
||||||
const getBuilderPushEndpoint = function (
|
const getBuilderPushEndpoint = function (
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
@ -75,10 +75,7 @@ const showPushProgress = function (message: string) {
|
|||||||
return progressBar;
|
return progressBar;
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadToPromise = (
|
const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
|
||||||
uploadRequest: ReturnType<typeof got.stream.post>,
|
|
||||||
logger: Logger,
|
|
||||||
) =>
|
|
||||||
new Promise<{ buildId: number }>(function (resolve, reject) {
|
new Promise<{ buildId: number }>(function (resolve, reject) {
|
||||||
uploadRequest.on('error', reject).on('data', function handleMessage(data) {
|
uploadRequest.on('error', reject).on('data', function handleMessage(data) {
|
||||||
let obj;
|
let obj;
|
||||||
@ -109,7 +106,10 @@ const uploadToPromise = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadImage = async function (
|
/**
|
||||||
|
* @returns {Promise<{ buildId: number }>}
|
||||||
|
*/
|
||||||
|
const uploadImage = function (
|
||||||
imageStream: NodeJS.ReadableStream & { length: number },
|
imageStream: NodeJS.ReadableStream & { length: number },
|
||||||
token: string,
|
token: string,
|
||||||
username: string,
|
username: string,
|
||||||
@ -117,9 +117,10 @@ const uploadImage = async function (
|
|||||||
appName: string,
|
appName: string,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<{ buildId: number }> {
|
): Promise<{ buildId: number }> {
|
||||||
const { default: got } = await import('got');
|
const request = require('request') as typeof import('request');
|
||||||
const progressStream = await import('progress-stream');
|
const progressStream =
|
||||||
const zlib = await import('zlib');
|
require('progress-stream') as typeof import('progress-stream');
|
||||||
|
const zlib = require('zlib') as typeof import('zlib');
|
||||||
|
|
||||||
// Need to strip off the newline
|
// Need to strip off the newline
|
||||||
const progressMessage = logger
|
const progressMessage = logger
|
||||||
@ -140,26 +141,25 @@ const uploadImage = async function (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const uploadRequest = got.stream.post(
|
const uploadRequest = request.post({
|
||||||
getBuilderPushEndpoint(url, username, appName),
|
url: getBuilderPushEndpoint(url, username, appName),
|
||||||
{
|
headers: {
|
||||||
headers: {
|
'Content-Encoding': 'gzip',
|
||||||
'Content-Encoding': 'gzip',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: streamWithProgress.pipe(
|
|
||||||
zlib.createGzip({
|
|
||||||
level: 6,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
throwHttpErrors: false,
|
|
||||||
},
|
},
|
||||||
);
|
auth: {
|
||||||
|
bearer: token,
|
||||||
|
},
|
||||||
|
body: streamWithProgress.pipe(
|
||||||
|
zlib.createGzip({
|
||||||
|
level: 6,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
return uploadToPromise(uploadRequest, logger);
|
return uploadToPromise(uploadRequest, logger);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadLogs = async function (
|
const uploadLogs = function (
|
||||||
logs: string,
|
logs: string,
|
||||||
token: string,
|
token: string,
|
||||||
url: string,
|
url: string,
|
||||||
@ -167,14 +167,14 @@ const uploadLogs = async function (
|
|||||||
username: string,
|
username: string,
|
||||||
appName: string,
|
appName: string,
|
||||||
) {
|
) {
|
||||||
const { default: got } = await import('got');
|
const request = require('request') as typeof import('request');
|
||||||
return got.post(getBuilderLogPushEndpoint(url, buildId, username, appName), {
|
return request.post({
|
||||||
headers: {
|
json: true,
|
||||||
Authorization: `Bearer ${token}`,
|
url: getBuilderLogPushEndpoint(url, buildId, username, appName),
|
||||||
|
auth: {
|
||||||
|
bearer: token,
|
||||||
},
|
},
|
||||||
body: Buffer.from(logs),
|
body: Buffer.from(logs),
|
||||||
responseType: 'json',
|
|
||||||
throwHttpErrors: false,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -232,7 +232,7 @@ export const deployLegacy = async function (
|
|||||||
username,
|
username,
|
||||||
appName,
|
appName,
|
||||||
]);
|
]);
|
||||||
await uploadLogs(...args);
|
uploadLogs(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildId;
|
return buildId;
|
||||||
|
@ -15,12 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
import * as request from 'request';
|
||||||
|
import type * as Stream from 'stream';
|
||||||
|
|
||||||
import { retry } from '../helpers';
|
import { retry } from '../helpers';
|
||||||
import Logger = require('../logger');
|
import Logger = require('../logger');
|
||||||
import * as ApiErrors from './errors';
|
import * as ApiErrors from './errors';
|
||||||
import { getBalenaSdk } from '../lazy';
|
|
||||||
import type { BalenaSDK } from 'balena-sdk';
|
|
||||||
|
|
||||||
export interface DeviceResponse {
|
export interface DeviceResponse {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@ -80,9 +80,9 @@ export class DeviceAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Either return nothing, or throw an error with the info
|
// Either return nothing, or throw an error with the info
|
||||||
public async setTargetState(state: Record<string, any>) {
|
public async setTargetState(state: any): Promise<void> {
|
||||||
const url = this.getUrlForAction('setTargetState');
|
const url = this.getUrlForAction('setTargetState');
|
||||||
await DeviceAPI.sendRequest(
|
return DeviceAPI.promisifiedRequest(
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url,
|
url,
|
||||||
@ -96,37 +96,37 @@ export class DeviceAPI {
|
|||||||
public async getTargetState() {
|
public async getTargetState() {
|
||||||
const url = this.getUrlForAction('getTargetState');
|
const url = this.getUrlForAction('getTargetState');
|
||||||
|
|
||||||
return await DeviceAPI.sendRequest(
|
return DeviceAPI.promisifiedRequest(
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
json: true,
|
json: true,
|
||||||
},
|
},
|
||||||
this.logger,
|
this.logger,
|
||||||
).then(({ state }: { state: Record<string, any> }) => {
|
).then((body) => {
|
||||||
return state;
|
return body.state;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDeviceInformation() {
|
public async getDeviceInformation(): Promise<DeviceInfo> {
|
||||||
const url = this.getUrlForAction('getDeviceInformation');
|
const url = this.getUrlForAction('getDeviceInformation');
|
||||||
|
|
||||||
return await DeviceAPI.sendRequest(
|
return DeviceAPI.promisifiedRequest(
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
json: true,
|
json: true,
|
||||||
},
|
},
|
||||||
this.logger,
|
this.logger,
|
||||||
).then(({ info }: { info: DeviceInfo }) => {
|
).then((body) => {
|
||||||
return info;
|
return body.info;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getContainerId(serviceName: string): Promise<string> {
|
public async getContainerId(serviceName: string): Promise<string> {
|
||||||
const url = this.getUrlForAction('containerId');
|
const url = this.getUrlForAction('containerId');
|
||||||
|
|
||||||
const body = await DeviceAPI.sendRequest(
|
const body = await DeviceAPI.promisifiedRequest(
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
@ -146,10 +146,10 @@ export class DeviceAPI {
|
|||||||
return body.containerId;
|
return body.containerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ping() {
|
public async ping(): Promise<void> {
|
||||||
const url = this.getUrlForAction('ping');
|
const url = this.getUrlForAction('ping');
|
||||||
|
|
||||||
await DeviceAPI.sendRequest(
|
return DeviceAPI.promisifiedRequest(
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
@ -158,10 +158,10 @@ export class DeviceAPI {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getVersion(): Promise<string> {
|
public getVersion(): Promise<string> {
|
||||||
const url = this.getUrlForAction('version');
|
const url = this.getUrlForAction('version');
|
||||||
|
|
||||||
return await DeviceAPI.sendRequest({
|
return DeviceAPI.promisifiedRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
json: true,
|
json: true,
|
||||||
@ -176,10 +176,10 @@ export class DeviceAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getStatus() {
|
public getStatus(): Promise<Status> {
|
||||||
const url = this.getUrlForAction('status');
|
const url = this.getUrlForAction('status');
|
||||||
|
|
||||||
return await DeviceAPI.sendRequest({
|
return DeviceAPI.promisifiedRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
json: true,
|
json: true,
|
||||||
@ -194,60 +194,96 @@ export class DeviceAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLogStream() {
|
public getLogStream(): Promise<Stream.Readable> {
|
||||||
const url = this.getUrlForAction('logs');
|
const url = this.getUrlForAction('logs');
|
||||||
const sdk = getBalenaSdk();
|
|
||||||
|
|
||||||
const stream = await sdk.request.stream({ url });
|
// Don't use the promisified version here as we want to stream the output
|
||||||
stream.on('response', (res) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (res.statusCode !== 200) {
|
const req = request.get(url);
|
||||||
throw new ApiErrors.DeviceAPIError(
|
|
||||||
'Non-200 response from log streaming endpoint',
|
req.on('error', reject).on('response', (res) => {
|
||||||
);
|
if (res.statusCode !== 200) {
|
||||||
}
|
reject(
|
||||||
res.socket.setKeepAlive(true, 1000);
|
new ApiErrors.DeviceAPIError(
|
||||||
|
'Non-200 response from log streaming endpoint',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
res.socket.setKeepAlive(true, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error as Error);
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return stream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUrlForAction(action: keyof typeof deviceEndpoints) {
|
private getUrlForAction(action: keyof typeof deviceEndpoints): string {
|
||||||
return `${this.deviceAddress}${deviceEndpoints[action]}`;
|
return `${this.deviceAddress}${deviceEndpoints[action]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A helper method for promisifying general (non-streaming) requests. Streaming
|
// A helper method for promisifying general (non-streaming) requests. Streaming
|
||||||
// requests should use a seperate setup
|
// requests should use a seperate setup
|
||||||
private static async sendRequest(
|
private static async promisifiedRequest<
|
||||||
opts: Parameters<BalenaSDK['request']['send']>[number],
|
T extends Parameters<typeof request>[0],
|
||||||
logger?: Logger,
|
>(opts: T, logger?: Logger): Promise<any> {
|
||||||
) {
|
interface ObjectWithUrl {
|
||||||
if (logger != null && opts.url != null) {
|
url?: string;
|
||||||
logger.logDebug(`Sending request to ${opts.url}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sdk = getBalenaSdk();
|
if (logger != null) {
|
||||||
const doRequest = async () => {
|
let url: string | null = null;
|
||||||
const response = await sdk.request.send(opts);
|
if (_.isObject(opts) && (opts as ObjectWithUrl).url != null) {
|
||||||
const bodyError =
|
// the `as string` shouldn't be necessary, but the type system
|
||||||
typeof response.body === 'string'
|
// is getting a little confused
|
||||||
? response.body
|
url = (opts as ObjectWithUrl).url!;
|
||||||
: response.body.message;
|
} else if (typeof opts === 'string') {
|
||||||
switch (response.statusCode) {
|
url = opts;
|
||||||
case 200:
|
|
||||||
return response.body;
|
|
||||||
case 400:
|
|
||||||
throw new ApiErrors.BadRequestDeviceAPIError(bodyError);
|
|
||||||
case 503:
|
|
||||||
throw new ApiErrors.ServiceUnavailableAPIError(bodyError);
|
|
||||||
default:
|
|
||||||
new ApiErrors.DeviceAPIError(bodyError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url != null) {
|
||||||
|
logger.logDebug(`Sending request to ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doRequest = async () => {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
return request(opts, (err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err as Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 200: {
|
||||||
|
resolve(body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 400: {
|
||||||
|
reject(new ApiErrors.BadRequestDeviceAPIError(body.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 503: {
|
||||||
|
reject(new ApiErrors.ServiceUnavailableAPIError(body.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
reject(new ApiErrors.DeviceAPIError(body.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return await retry({
|
return await retry({
|
||||||
func: doRequest,
|
func: doRequest,
|
||||||
initialDelayMs: 2000,
|
initialDelayMs: 2000,
|
||||||
maxAttempts: 6,
|
maxAttempts: 6,
|
||||||
label: `Supervisor API (${opts.method} ${opts.url})`,
|
label: `Supervisor API (${opts.method} ${(opts as any).url})`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default DeviceAPI;
|
||||||
|
@ -603,11 +603,11 @@ function getImageNameFromTask(task: BuildTask): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateTargetState(
|
export function generateTargetState(
|
||||||
currentTargetState: Record<string, any>,
|
currentTargetState: any,
|
||||||
composition: Composition,
|
composition: Composition,
|
||||||
buildTasks: BuildTask[],
|
buildTasks: BuildTask[],
|
||||||
env: ParsedEnvironment,
|
env: ParsedEnvironment,
|
||||||
) {
|
): any {
|
||||||
const keyedBuildTasks = _.keyBy(buildTasks, 'serviceName');
|
const keyedBuildTasks = _.keyBy(buildTasks, 'serviceName');
|
||||||
|
|
||||||
const services: { [serviceId: string]: any } = {};
|
const services: { [serviceId: string]: any } = {};
|
||||||
|
@ -28,7 +28,7 @@ import { instanceOf } from '../../errors';
|
|||||||
import Logger = require('../logger');
|
import Logger = require('../logger');
|
||||||
|
|
||||||
import { Dockerfile } from 'livepush';
|
import { Dockerfile } from 'livepush';
|
||||||
import type { DeviceAPI } from './api';
|
import type DeviceAPI from './api';
|
||||||
import type { DeviceInfo, Status } from './api';
|
import type { DeviceInfo, Status } from './api';
|
||||||
import type { DeviceDeployOptions } from './deploy';
|
import type { DeviceDeployOptions } from './deploy';
|
||||||
import { generateTargetState, rebuildSingleTask } from './deploy';
|
import { generateTargetState, rebuildSingleTask } from './deploy';
|
||||||
|
@ -110,27 +110,6 @@ export async function getManifest(
|
|||||||
const init = await import('balena-device-init');
|
const init = await import('balena-device-init');
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
const manifest = await init.getImageManifest(image);
|
const manifest = await init.getImageManifest(image);
|
||||||
if (manifest != null) {
|
|
||||||
const config = manifest.configuration?.config;
|
|
||||||
if (config?.partition != null) {
|
|
||||||
const { getBootPartition } = await import('balena-config-json');
|
|
||||||
// Find the device-type.json property that holds the boot partition number for
|
|
||||||
// this device type (config.partition or config.partition.primary) and overwrite it
|
|
||||||
// with the boot partition number that was found by inspecting the image.
|
|
||||||
// since it's deprecated & no longer updated for newer releases.
|
|
||||||
if (typeof config.partition === 'number') {
|
|
||||||
config.partition = await getBootPartition(image);
|
|
||||||
} else if (config.partition.primary != null) {
|
|
||||||
config.partition.primary = await getBootPartition(image);
|
|
||||||
}
|
|
||||||
// TODO: Add handling for when we no longer include a `config.partition` at all.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: Change this in the next major to throw, after confirming that this works for all supported OS versions.
|
|
||||||
console.error(
|
|
||||||
`[warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
manifest != null &&
|
manifest != null &&
|
||||||
manifest.slug !== deviceType &&
|
manifest.slug !== deviceType &&
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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);
|
|
||||||
});
|
|
||||||
}
|
|
@ -76,28 +76,38 @@ export const getImagePath = async (deviceType: string, version?: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Determine if a device image is cached
|
* @summary Determine if a device image is fresh
|
||||||
*
|
*
|
||||||
* @description
|
* @description
|
||||||
* If the device image does not exist, return false.
|
* If the device image does not exist, return false.
|
||||||
*
|
*
|
||||||
* @param {String} deviceType - device type slug or alias
|
* @param {String} deviceType - device type slug or alias
|
||||||
* @param {String} version - the exact balenaOS version number
|
* @param {String} version - the exact balenaOS version number
|
||||||
* @returns {Promise<Boolean>} is image cached
|
* @returns {Promise<Boolean>} is image fresh
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* isImageCached ('raspberry-pi', '1.2.3').then (isCached) ->
|
* isImageFresh('raspberry-pi', '1.2.3').then (isFresh) ->
|
||||||
* if isCached
|
* if isFresh
|
||||||
* console.log('The Raspberry Pi image v1.2.3 is cached!')
|
* console.log('The Raspberry Pi image v1.2.3 is fresh!')
|
||||||
*/
|
*/
|
||||||
export const isImageCached = async (deviceType: string, version: string) => {
|
export const isImageFresh = async (deviceType: string, version: string) => {
|
||||||
const imagePath = await getImagePath(deviceType, version);
|
const imagePath = await getImagePath(deviceType, version);
|
||||||
|
let createdDate;
|
||||||
try {
|
try {
|
||||||
const createdDate = await getFileCreatedDate(imagePath);
|
createdDate = await getFileCreatedDate(imagePath);
|
||||||
return createdDate != null;
|
|
||||||
} catch {
|
} catch {
|
||||||
|
// Swallow errors from getFileCreatedTime.
|
||||||
|
}
|
||||||
|
if (createdDate == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const balena = getBalenaSdk();
|
||||||
|
const lastModifiedDate = await balena.models.os.getLastModified(
|
||||||
|
deviceType,
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
return lastModifiedDate < createdDate;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -276,7 +286,7 @@ export const getStream = async (
|
|||||||
versionOrRange = 'latest';
|
versionOrRange = 'latest';
|
||||||
}
|
}
|
||||||
const version = await resolveVersion(deviceType, versionOrRange);
|
const version = await resolveVersion(deviceType, versionOrRange);
|
||||||
const isFresh = await isImageCached(deviceType, version);
|
const isFresh = await isImageFresh(deviceType, version);
|
||||||
const $stream = isFresh
|
const $stream = isFresh
|
||||||
? await getImage(deviceType, version)
|
? await getImage(deviceType, version)
|
||||||
: await doDownload({ ...options, deviceType, version });
|
: await doDownload({ ...options, deviceType, version });
|
||||||
|
@ -21,7 +21,6 @@ import type { Chalk } from 'chalk';
|
|||||||
import type * as visuals from 'resin-cli-visuals';
|
import type * as visuals from 'resin-cli-visuals';
|
||||||
import type * as CliForm from 'resin-cli-form';
|
import type * as CliForm from 'resin-cli-form';
|
||||||
import type { ux } from '@oclif/core';
|
import type { ux } from '@oclif/core';
|
||||||
import { version } from '../../package.json';
|
|
||||||
|
|
||||||
// Equivalent of _.once but avoiding the need to import lodash for lazy deps
|
// Equivalent of _.once but avoiding the need to import lodash for lazy deps
|
||||||
const once = <T>(fn: () => T) => {
|
const once = <T>(fn: () => T) => {
|
||||||
@ -44,26 +43,9 @@ export const onceAsync = <T>(fn: () => Promise<T>) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const cliXBalenaClientHeaderInterceptor: BalenaSdk.Interceptor = {
|
export const getBalenaSdk = once(() =>
|
||||||
request($request) {
|
(require('balena-sdk') as typeof BalenaSdk).fromSharedOptions(),
|
||||||
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(
|
export const getVisuals = once(
|
||||||
() => require('resin-cli-visuals') as typeof visuals,
|
() => require('resin-cli-visuals') as typeof visuals,
|
||||||
|
@ -159,9 +159,9 @@ especially discouraged in scripts (e.g. CI environments).`;
|
|||||||
|
|
||||||
export const devModeInfo = `\
|
export const devModeInfo = `\
|
||||||
The '--dev' option is used to configure balenaOS to operate in development mode,
|
The '--dev' option is used to configure balenaOS to operate in development mode,
|
||||||
allowing unauthenticated root ssh access and exposing network ports such as
|
allowing anauthenticated root ssh access and exposing network ports such as
|
||||||
balenaEngine's 2375 (unencrypted). This option causes \`"developmentMode": true\`
|
balenaEngine's 2375 (unencrypted). This option causes \`"developmentMode": true\`
|
||||||
to be inserted in the 'config.json' file in the image's boot partition. Development
|
to be inserted in the 'config.json' file in the image's boot partion. Development
|
||||||
mode (as a configurable option) is applicable to balenaOS releases from early
|
mode (as a configurable option) is applicable to balenaOS releases from early
|
||||||
2022. Older releases have separate development and production balenaOS images
|
2022. Older releases have separate development and production balenaOS images
|
||||||
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
|
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
|
||||||
|
@ -50,7 +50,7 @@ export function copyQemu(context: string, arch: string) {
|
|||||||
.then(() => getQemuPath(arch))
|
.then(() => getQemuPath(arch))
|
||||||
.then(
|
.then(
|
||||||
(qemu) =>
|
(qemu) =>
|
||||||
new Promise<void>(function (resolve, reject) {
|
new Promise(function (resolve, reject) {
|
||||||
const read = fs.createReadStream(qemu);
|
const read = fs.createReadStream(qemu);
|
||||||
const write = fs.createWriteStream(binPath);
|
const write = fs.createWriteStream(binPath);
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ async function installQemu(arch: string, qemuPath: string) {
|
|||||||
const urlVersion = encodeURIComponent(QEMU_VERSION);
|
const urlVersion = encodeURIComponent(QEMU_VERSION);
|
||||||
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`;
|
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`;
|
||||||
|
|
||||||
const { default: got } = await import('got');
|
const request = await import('request');
|
||||||
const fs = await import('fs');
|
const fs = await import('fs');
|
||||||
const zlib = await import('zlib');
|
const zlib = await import('zlib');
|
||||||
const tar = await import('tar-stream');
|
const tar = await import('tar-stream');
|
||||||
@ -117,8 +117,7 @@ async function installQemu(arch: string, qemuPath: string) {
|
|||||||
reject(err as Error);
|
reject(err as Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
got.stream
|
request(qemuUrl)
|
||||||
.get(qemuUrl)
|
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.pipe(zlib.createGunzip())
|
.pipe(zlib.createGunzip())
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
|
@ -16,8 +16,7 @@ limitations under the License.
|
|||||||
import type { BalenaSDK } from 'balena-sdk';
|
import type { BalenaSDK } from 'balena-sdk';
|
||||||
import * as JSONStream from 'JSONStream';
|
import * as JSONStream from 'JSONStream';
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import type { PlainResponse } from 'got';
|
import * as request from 'request';
|
||||||
import type got from 'got';
|
|
||||||
import type { RegistrySecrets } from '@balena/compose/dist/multibuild';
|
import type { RegistrySecrets } from '@balena/compose/dist/multibuild';
|
||||||
import type * as Stream from 'stream';
|
import type * as Stream from 'stream';
|
||||||
import streamToPromise = require('stream-to-promise');
|
import streamToPromise = require('stream-to-promise');
|
||||||
@ -120,7 +119,7 @@ export async function startRemoteBuild(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err.message);
|
console.error(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
buildRequest.destroy();
|
buildRequest.abort();
|
||||||
const sigintErr = new SIGINTError('Build aborted on SIGINT signal');
|
const sigintErr = new SIGINTError('Build aborted on SIGINT signal');
|
||||||
sigintErr.code = 'SIGINT';
|
sigintErr.code = 'SIGINT';
|
||||||
stream.emit('error', sigintErr);
|
stream.emit('error', sigintErr);
|
||||||
@ -338,29 +337,32 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
|
|||||||
/**
|
/**
|
||||||
* Initiate a POST HTTP request to the remote builder and add some event
|
* Initiate a POST HTTP request to the remote builder and add some event
|
||||||
* listeners.
|
* listeners.
|
||||||
|
*
|
||||||
|
* ¡! Note: this function must be synchronous because of a bug in the `request`
|
||||||
|
* library that requires the following two steps to take place in the same
|
||||||
|
* iteration of Node's event loop: (1) adding a listener for the 'response'
|
||||||
|
* event and (2) calling request.pipe():
|
||||||
|
* https://github.com/request/request/issues/887
|
||||||
*/
|
*/
|
||||||
async function createRemoteBuildRequest(
|
function createRemoteBuildRequest(
|
||||||
build: RemoteBuild,
|
build: RemoteBuild,
|
||||||
tarStream: Stream.Readable,
|
tarStream: Stream.Readable,
|
||||||
builderUrl: string,
|
builderUrl: string,
|
||||||
onError: (error: Error) => void,
|
onError: (error: Error) => void,
|
||||||
) {
|
): request.Request {
|
||||||
const { default: got } = await import('got');
|
const zlib = require('zlib') as typeof import('zlib');
|
||||||
const zlib = await import('zlib');
|
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
console.error(`[debug] Connecting to builder at ${builderUrl}`);
|
console.error(`[debug] Connecting to builder at ${builderUrl}`);
|
||||||
}
|
}
|
||||||
return got.stream
|
return request
|
||||||
.post(builderUrl, {
|
.post({
|
||||||
headers: {
|
url: builderUrl,
|
||||||
Authorization: `Bearer ${build.auth}`,
|
auth: { bearer: build.auth },
|
||||||
'Content-Encoding': 'gzip',
|
headers: { 'Content-Encoding': 'gzip' },
|
||||||
},
|
|
||||||
body: tarStream.pipe(zlib.createGzip({ level: 6 })),
|
body: tarStream.pipe(zlib.createGzip({ level: 6 })),
|
||||||
throwHttpErrors: false,
|
|
||||||
})
|
})
|
||||||
.once('error', onError) // `.once` because the handler re-emits
|
.once('error', onError) // `.once` because the handler re-emits
|
||||||
.once('response', (response: PlainResponse) => {
|
.once('response', (response: request.RequestResponse) => {
|
||||||
if (response.statusCode >= 100 && response.statusCode < 400) {
|
if (response.statusCode >= 100 && response.statusCode < 400) {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
console.error(
|
console.error(
|
||||||
@ -372,8 +374,8 @@ async function createRemoteBuildRequest(
|
|||||||
'Remote builder responded with HTTP error:',
|
'Remote builder responded with HTTP error:',
|
||||||
`${response.statusCode} ${response.statusMessage}`,
|
`${response.statusCode} ${response.statusMessage}`,
|
||||||
];
|
];
|
||||||
if (response.rawBody) {
|
if (response.body) {
|
||||||
msgArr.push(response.rawBody.toString());
|
msgArr.push(response.body);
|
||||||
}
|
}
|
||||||
onError(new ExpectedError(msgArr.join('\n')));
|
onError(new ExpectedError(msgArr.join('\n')));
|
||||||
}
|
}
|
||||||
@ -382,7 +384,7 @@ async function createRemoteBuildRequest(
|
|||||||
|
|
||||||
async function getRemoteBuildStream(
|
async function getRemoteBuildStream(
|
||||||
build: RemoteBuild,
|
build: RemoteBuild,
|
||||||
): Promise<[ReturnType<typeof got.stream.post>, Stream.Stream]> {
|
): Promise<[request.Request, Stream.Stream]> {
|
||||||
const builderUrl = await getBuilderEndpoint(
|
const builderUrl = await getBuilderEndpoint(
|
||||||
build.baseUrl,
|
build.baseUrl,
|
||||||
build.appSlug,
|
build.appSlug,
|
||||||
@ -410,7 +412,7 @@ async function getRemoteBuildStream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tarStream = await getTarStream(build);
|
const tarStream = await getTarStream(build);
|
||||||
const buildRequest = await createRemoteBuildRequest(
|
const buildRequest = createRemoteBuildRequest(
|
||||||
build,
|
build,
|
||||||
tarStream,
|
tarStream,
|
||||||
builderUrl,
|
builderUrl,
|
||||||
|
@ -20,7 +20,7 @@ import * as chaiAsPromised from 'chai-as-promised';
|
|||||||
import * as ejs from 'ejs';
|
import * as ejs from 'ejs';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import got from 'got';
|
import * as request from 'request';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
import { LoginServer } from '../../build/auth/server';
|
import { LoginServer } from '../../build/auth/server';
|
||||||
@ -61,30 +61,38 @@ describe('Login server:', function () {
|
|||||||
server.shutdown();
|
server.shutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function testLogin({
|
async function testLogin(opt: {
|
||||||
verb = 'post',
|
|
||||||
...opt
|
|
||||||
}: {
|
|
||||||
expectedBody: string;
|
expectedBody: string;
|
||||||
expectedErrorMsg?: string;
|
expectedErrorMsg?: string;
|
||||||
expectedStatusCode: number;
|
expectedStatusCode: number;
|
||||||
expectedToken: string;
|
expectedToken: string;
|
||||||
urlPath?: string;
|
urlPath?: string;
|
||||||
verb?: 'post' | 'put';
|
verb?: string;
|
||||||
}) {
|
}) {
|
||||||
opt.urlPath = opt.urlPath ?? addr.urlPath;
|
opt.urlPath = opt.urlPath ?? addr.urlPath;
|
||||||
const res = await got[verb](
|
const post = opt.verb
|
||||||
`http://${addr.host}:${addr.port}${opt.urlPath}`,
|
? ((request as any)[opt.verb] as typeof request.post)
|
||||||
{
|
: request.post;
|
||||||
form: {
|
await new Promise<void>((resolve, reject) => {
|
||||||
token: opt.expectedToken,
|
post(
|
||||||
|
`http://${addr.host}:${addr.port}${opt.urlPath}`,
|
||||||
|
{
|
||||||
|
form: {
|
||||||
|
token: opt.expectedToken,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
throwHttpErrors: false,
|
function (error, response, body) {
|
||||||
},
|
try {
|
||||||
);
|
expect(error).to.not.exist;
|
||||||
|
expect(response.statusCode).to.equal(opt.expectedStatusCode);
|
||||||
expect(res.body).to.equal(opt.expectedBody);
|
expect(body).to.equal(opt.expectedBody);
|
||||||
expect(res.statusCode).to.equal(opt.expectedStatusCode);
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
reject(err as Error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = await server.awaitForToken();
|
const token = await server.awaitForToken();
|
||||||
@ -119,7 +127,7 @@ describe('Login server:', function () {
|
|||||||
expectedStatusCode: 404,
|
expectedStatusCode: 404,
|
||||||
expectedToken: tokens.johndoe.token,
|
expectedToken: tokens.johndoe.token,
|
||||||
expectedErrorMsg: 'Unknown path or verb',
|
expectedErrorMsg: 'Unknown path or verb',
|
||||||
verb: 'put',
|
verb: 'get',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -442,93 +442,6 @@ describe('balena build', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create the expected tar stream (docker-compose --nologs)', async () => {
|
|
||||||
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
|
|
||||||
const service1Dockerfile = (
|
|
||||||
await fs.readFile(
|
|
||||||
path.join(projectPath, 'service1', 'Dockerfile.template'),
|
|
||||||
'utf8',
|
|
||||||
)
|
|
||||||
).replace('%%BALENA_MACHINE_NAME%%', 'nuc');
|
|
||||||
const expectedFilesByService: ExpectedTarStreamFilesByService = {
|
|
||||||
service1: {
|
|
||||||
Dockerfile: {
|
|
||||||
contents: service1Dockerfile,
|
|
||||||
fileSize: service1Dockerfile.length,
|
|
||||||
type: 'file',
|
|
||||||
},
|
|
||||||
'Dockerfile.template': { fileSize: 144, type: 'file' },
|
|
||||||
'file1.sh': { fileSize: 12, type: 'file' },
|
|
||||||
},
|
|
||||||
service2: {
|
|
||||||
'Dockerfile-alt': { fileSize: 13, type: 'file' },
|
|
||||||
'file2-crlf.sh': {
|
|
||||||
fileSize: isWindows ? 12 : 14,
|
|
||||||
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
|
||||||
type: 'file',
|
|
||||||
},
|
|
||||||
'src/file1.sh': { fileSize: 12, type: 'file' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const responseFilename = 'build-POST.json';
|
|
||||||
const responseBody = await fs.readFile(
|
|
||||||
path.join(dockerResponsePath, responseFilename),
|
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
const expectedQueryParamsByService = {
|
|
||||||
service1: Object.entries(
|
|
||||||
_.merge({}, commonComposeQueryParams, {
|
|
||||||
buildargs: {
|
|
||||||
COMPOSE_ARG: 'A',
|
|
||||||
barg: 'b',
|
|
||||||
SERVICE1_VAR: 'This is a service specific variable',
|
|
||||||
},
|
|
||||||
cachefrom: ['my/img1', 'my/img2'],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
service2: Object.entries(
|
|
||||||
_.merge({}, commonComposeQueryParamsIntel, {
|
|
||||||
buildargs: {
|
|
||||||
COMPOSE_ARG: 'A',
|
|
||||||
barg: 'b',
|
|
||||||
},
|
|
||||||
cachefrom: ['my/img1', 'my/img2'],
|
|
||||||
dockerfile: 'Dockerfile-alt',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
const expectedResponseLines: string[] = [
|
|
||||||
...commonResponseLines[responseFilename],
|
|
||||||
...getDockerignoreWarn1(
|
|
||||||
[path.join(projectPath, 'service2', '.dockerignore')],
|
|
||||||
'build',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
if (isWindows) {
|
|
||||||
expectedResponseLines.push(
|
|
||||||
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
|
|
||||||
projectPath,
|
|
||||||
'service2',
|
|
||||||
'file2-crlf.sh',
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
docker.expectGetInfo({});
|
|
||||||
docker.expectGetManifestNucAlpine();
|
|
||||||
docker.expectGetManifestBusybox();
|
|
||||||
await testDockerBuildStream({
|
|
||||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2 --nologs`,
|
|
||||||
dockerMock: docker,
|
|
||||||
expectedFilesByService,
|
|
||||||
expectedQueryParamsByService,
|
|
||||||
expectedResponseLines,
|
|
||||||
projectPath,
|
|
||||||
responseBody,
|
|
||||||
responseCode: 200,
|
|
||||||
services: ['service1', 'service2'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => {
|
it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => {
|
||||||
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
|
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
|
||||||
const service1Dockerfile = (
|
const service1Dockerfile = (
|
||||||
|
@ -27,7 +27,7 @@ import * as sinon from 'sinon';
|
|||||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||||
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
||||||
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
|
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
|
||||||
import { cleanOutput, runCommand } from '../helpers';
|
import { cleanOutput, runCommand, switchSentry } from '../helpers';
|
||||||
import type {
|
import type {
|
||||||
ExpectedTarStreamFiles,
|
ExpectedTarStreamFiles,
|
||||||
ExpectedTarStreamFilesByService,
|
ExpectedTarStreamFilesByService,
|
||||||
@ -262,6 +262,7 @@ describe('balena deploy', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update a release with status="failed" on error (single container)', async () => {
|
it('should update a release with status="failed" on error (single container)', async () => {
|
||||||
|
let sentryStatus: boolean | undefined;
|
||||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||||
const expectedFiles: ExpectedTarStreamFiles = {
|
const expectedFiles: ExpectedTarStreamFiles = {
|
||||||
'src/.dockerignore': { fileSize: 16, type: 'file' },
|
'src/.dockerignore': { fileSize: 16, type: 'file' },
|
||||||
@ -318,6 +319,7 @@ describe('balena deploy', function () {
|
|||||||
api.expectPostImageLabel();
|
api.expectPostImageLabel();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
sentryStatus = await switchSentry(false);
|
||||||
sinon.stub(process, 'exit');
|
sinon.stub(process, 'exit');
|
||||||
|
|
||||||
await testDockerBuildStream({
|
await testDockerBuildStream({
|
||||||
@ -335,8 +337,9 @@ describe('balena deploy', function () {
|
|||||||
});
|
});
|
||||||
expect(failedImagePatchRequests).to.equal(maxRequestRetries);
|
expect(failedImagePatchRequests).to.equal(maxRequestRetries);
|
||||||
} finally {
|
} finally {
|
||||||
// We mock process.exit and need to force cast it to a SinonStub to restore it
|
await switchSentry(sentryStatus);
|
||||||
(process.exit as unknown as sinon.SinonStub).restore();
|
// @ts-expect-error claims restore does not exist
|
||||||
|
process.exit.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -114,14 +114,6 @@ describe('balena device', function () {
|
|||||||
'Content-Type': 'application/json',
|
'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');
|
const { out, err } = await runCommand('device 27fda508c --json');
|
||||||
expect(err).to.be.empty;
|
expect(err).to.be.empty;
|
||||||
const json = JSON.parse(out.join(''));
|
const json = JSON.parse(out.join(''));
|
||||||
|
@ -21,6 +21,8 @@ import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
|||||||
import { cleanOutput, runCommand } from '../../helpers';
|
import { cleanOutput, runCommand } from '../../helpers';
|
||||||
import { SupervisorMock } from '../../nock/supervisor-mock';
|
import { SupervisorMock } from '../../nock/supervisor-mock';
|
||||||
|
|
||||||
|
const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip;
|
||||||
|
|
||||||
describe('balena device logs', function () {
|
describe('balena device logs', function () {
|
||||||
let api: BalenaAPIMock;
|
let api: BalenaAPIMock;
|
||||||
let supervisor: SupervisorMock;
|
let supervisor: SupervisorMock;
|
||||||
@ -37,7 +39,10 @@ describe('balena device logs', function () {
|
|||||||
supervisor.done();
|
supervisor.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reach the expected endpoints on a local device', async () => {
|
// skip non-standalone tests because nock's mock socket causes the error:
|
||||||
|
// "setKeepAliveInterval expects an instance of socket as its first argument"
|
||||||
|
// in utils/device/api.ts: NetKeepalive.setKeepAliveInterval(sock, 5000);
|
||||||
|
itS('should reach the expected endpoints on a local device', async () => {
|
||||||
supervisor.expectGetPing();
|
supervisor.expectGetPing();
|
||||||
supervisor.expectGetLogs();
|
supervisor.expectGetLogs();
|
||||||
supervisor.expectGetLogs();
|
supervisor.expectGetLogs();
|
||||||
|
@ -22,7 +22,6 @@ import { runCommand } from '../../helpers';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import * as tmp from 'tmp';
|
import * as tmp from 'tmp';
|
||||||
import type * as $imagefs from 'balena-image-fs';
|
import type * as $imagefs from 'balena-image-fs';
|
||||||
import * as stripIndent from 'common-tags/lib/stripIndent';
|
|
||||||
|
|
||||||
tmp.setGracefulCleanup();
|
tmp.setGracefulCleanup();
|
||||||
const tmpNameAsync = promisify(tmp.tmpName);
|
const tmpNameAsync = promisify(tmp.tmpName);
|
||||||
@ -35,7 +34,6 @@ if (process.platform !== 'win32') {
|
|||||||
let api: BalenaAPIMock;
|
let api: BalenaAPIMock;
|
||||||
let tmpDummyPath: string;
|
let tmpDummyPath: string;
|
||||||
let tmpMatchingDtJsonPartitionPath: string;
|
let tmpMatchingDtJsonPartitionPath: string;
|
||||||
let tmpNonMatchingDtJsonPartitionPath: string;
|
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
// We conditionally import balena-image-fs, since when imported on top level then unrelated tests on win32 failed with:
|
// We conditionally import balena-image-fs, since when imported on top level then unrelated tests on win32 failed with:
|
||||||
@ -49,48 +47,6 @@ if (process.platform !== 'win32') {
|
|||||||
'./tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img',
|
'./tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img',
|
||||||
tmpMatchingDtJsonPartitionPath,
|
tmpMatchingDtJsonPartitionPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
tmpNonMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string;
|
|
||||||
// Create an image with a device-type.json that mentions a non matching boot partition.
|
|
||||||
// We copy the pre-existing image and modify it, since including a separate one
|
|
||||||
// would add 18MB more to the repository.
|
|
||||||
await fs.copyFile(
|
|
||||||
'./tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img',
|
|
||||||
tmpNonMatchingDtJsonPartitionPath,
|
|
||||||
);
|
|
||||||
await imagefs.interact(
|
|
||||||
tmpNonMatchingDtJsonPartitionPath,
|
|
||||||
12,
|
|
||||||
async (_fs) => {
|
|
||||||
const dtJson = JSON.parse(
|
|
||||||
await _fs.promises.readFile('/device-type.json', {
|
|
||||||
encoding: 'utf8',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(dtJson).to.have.nested.property(
|
|
||||||
'configuration.config.partition',
|
|
||||||
12,
|
|
||||||
);
|
|
||||||
dtJson.configuration.config.partition = 999;
|
|
||||||
await _fs.promises.writeFile(
|
|
||||||
'/device-type.json',
|
|
||||||
JSON.stringify(dtJson),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _fs.promises.writeFile(
|
|
||||||
'/os-release',
|
|
||||||
stripIndent`
|
|
||||||
ID="balena-os"
|
|
||||||
NAME="balenaOS"
|
|
||||||
VERSION="6.1.25"
|
|
||||||
VERSION_ID="6.1.25"
|
|
||||||
PRETTY_NAME="balenaOS 6.1.25"
|
|
||||||
DISTRO_CODENAME="kirkstone"
|
|
||||||
MACHINE="jetson-nano"
|
|
||||||
META_BALENA_VERSION="6.1.25"`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -105,20 +61,20 @@ if (process.platform !== 'win32') {
|
|||||||
after(async () => {
|
after(async () => {
|
||||||
await fs.unlink(tmpDummyPath);
|
await fs.unlink(tmpDummyPath);
|
||||||
await fs.unlink(tmpMatchingDtJsonPartitionPath);
|
await fs.unlink(tmpMatchingDtJsonPartitionPath);
|
||||||
await fs.unlink(tmpNonMatchingDtJsonPartitionPath);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect the OS version and inject a valid config.json file to a 6.0.13 image with partition 12 as boot & matching device-type.json', async () => {
|
it('should inject a valid config.json file to an image with partition 12 as boot & matching device-type.json ', async () => {
|
||||||
api.expectGetApplication();
|
api.expectGetApplication();
|
||||||
api.expectGetDeviceTypes();
|
api.expectGetDeviceTypes();
|
||||||
// It should not reach to /config or /device-types/v1 but instead find
|
// TODO: this shouldn't be necessary & the CLI should be able to find
|
||||||
// everything required from the device-type.json in the image.
|
// everything required from the device-type.json in the image.
|
||||||
// api.expectGetConfigDeviceTypes();
|
api.expectGetConfigDeviceTypes();
|
||||||
api.expectDownloadConfig();
|
api.expectDownloadConfig();
|
||||||
|
|
||||||
const command: string[] = [
|
const command: string[] = [
|
||||||
`os configure ${tmpMatchingDtJsonPartitionPath}`,
|
`os configure ${tmpMatchingDtJsonPartitionPath}`,
|
||||||
'--device-type jetson-nano',
|
'--device-type jetson-nano',
|
||||||
|
'--version 6.0.13',
|
||||||
'--fleet testApp',
|
'--fleet testApp',
|
||||||
'--config-app-update-poll-interval 10',
|
'--config-app-update-poll-interval 10',
|
||||||
'--config-network ethernet',
|
'--config-network ethernet',
|
||||||
@ -135,65 +91,16 @@ if (process.platform !== 'win32') {
|
|||||||
tmpMatchingDtJsonPartitionPath,
|
tmpMatchingDtJsonPartitionPath,
|
||||||
12,
|
12,
|
||||||
async (_fs) => {
|
async (_fs) => {
|
||||||
|
const readFileAsync = promisify(_fs.readFile);
|
||||||
const dtJson = JSON.parse(
|
const dtJson = JSON.parse(
|
||||||
await _fs.promises.readFile('/device-type.json', {
|
await readFileAsync('/device-type.json', { encoding: 'utf8' }),
|
||||||
encoding: 'utf8',
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
// confirm that the device-type.json mentions the expected partition
|
// confirm that the device-type.json mentions the expected partition
|
||||||
expect(dtJson).to.have.nested.property(
|
expect(dtJson).to.have.nested.property(
|
||||||
'configuration.config.partition',
|
'configuration.config.partition',
|
||||||
12,
|
12,
|
||||||
);
|
);
|
||||||
return await _fs.promises.readFile('/config.json');
|
return await readFileAsync('/config.json');
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(config).to.not.be.empty;
|
|
||||||
|
|
||||||
// confirm the image has the correct config.json values...
|
|
||||||
const configObj = JSON.parse(config.toString('utf8'));
|
|
||||||
expect(configObj).to.have.property('deviceType', 'jetson-nano');
|
|
||||||
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect the OS version and inject a valid config.json file to a 6.1.25 image with partition 12 as boot & a non-matching device-type.json', async () => {
|
|
||||||
api.expectGetApplication();
|
|
||||||
api.expectGetDeviceTypes();
|
|
||||||
// It should not reach to /config or /device-types/v1 but instead find
|
|
||||||
// everything required from the device-type.json in the image.
|
|
||||||
// api.expectGetConfigDeviceTypes();
|
|
||||||
api.expectDownloadConfig();
|
|
||||||
|
|
||||||
const command: string[] = [
|
|
||||||
`os configure ${tmpNonMatchingDtJsonPartitionPath}`,
|
|
||||||
'--device-type jetson-nano',
|
|
||||||
'--fleet testApp',
|
|
||||||
'--config-app-update-poll-interval 10',
|
|
||||||
'--config-network ethernet',
|
|
||||||
'--initial-device-name testDeviceName',
|
|
||||||
'--provisioning-key-name testKey',
|
|
||||||
'--provisioning-key-expiry-date 2050-12-12',
|
|
||||||
];
|
|
||||||
|
|
||||||
const { err } = await runCommand(command.join(' '));
|
|
||||||
expect(err.join('')).to.equal('');
|
|
||||||
|
|
||||||
// confirm the image contains a config.json...
|
|
||||||
const config = await imagefs.interact(
|
|
||||||
tmpNonMatchingDtJsonPartitionPath,
|
|
||||||
12,
|
|
||||||
async (_fs) => {
|
|
||||||
const dtJson = JSON.parse(
|
|
||||||
await _fs.promises.readFile('/device-type.json', {
|
|
||||||
encoding: 'utf8',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// confirm that the device-type.json mentions the expected partition
|
|
||||||
expect(dtJson).to.have.nested.property(
|
|
||||||
'configuration.config.partition',
|
|
||||||
999,
|
|
||||||
);
|
|
||||||
return await _fs.promises.readFile('/config.json');
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(config).to.not.be.empty;
|
expect(config).to.not.be.empty;
|
||||||
@ -227,28 +134,11 @@ if (process.platform !== 'win32') {
|
|||||||
const { err } = await runCommand(command.join(' '));
|
const { err } = await runCommand(command.join(' '));
|
||||||
// Once we replace the dummy.img with one that includes a os-release & device-type.json
|
// Once we replace the dummy.img with one that includes a os-release & device-type.json
|
||||||
// then we should be able to change this to expect no errors.
|
// then we should be able to change this to expect no errors.
|
||||||
expect(
|
expect(err.join('')).to.equal('');
|
||||||
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'.
|
|
||||||
[warn] "${tmpDummyPath}":
|
|
||||||
[warn] Could not find a previous "/config.json" file in partition '1'.
|
|
||||||
[warn] Proceeding anyway, but this is unexpected.
|
|
||||||
[warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`.split(
|
|
||||||
'\n',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// confirm the image contains a config.json...
|
// confirm the image contains a config.json...
|
||||||
const config = await imagefs.interact(tmpDummyPath, 1, async (_fs) => {
|
const config = await imagefs.interact(tmpDummyPath, 1, async (_fs) => {
|
||||||
return await _fs.promises.readFile('/config.json');
|
return await promisify(_fs.readFile)('/config.json');
|
||||||
});
|
});
|
||||||
expect(config).to.not.be.empty;
|
expect(config).to.not.be.empty;
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ describe('balena release', function () {
|
|||||||
expect(err).to.be.empty;
|
expect(err).to.be.empty;
|
||||||
const json = JSON.parse(out.join(''));
|
const json = JSON.parse(out.join(''));
|
||||||
expect(json[0].commit).to.equal('90247b54de4fa7a0a3cbc85e73c68039');
|
expect(json[0].commit).to.equal('90247b54de4fa7a0a3cbc85e73c68039');
|
||||||
expect(json[0].release_image[0].image[0].start_timestamp).to.equal(
|
expect(json[0].contains__image[0].image[0].start_timestamp).to.equal(
|
||||||
'2020-01-04T01:13:08.583Z',
|
'2020-01-04T01:13:08.583Z',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -174,9 +174,19 @@ async function runCommandInSubprocess(
|
|||||||
};
|
};
|
||||||
const { exec } = await import('child_process');
|
const { exec } = await import('child_process');
|
||||||
|
|
||||||
|
// check if standalonePath exists and print its content
|
||||||
|
if (!fs.existsSync(standalonePath)) {
|
||||||
|
console.error('standalonePath does not exist');
|
||||||
|
} else {
|
||||||
|
console.error('file exists!');
|
||||||
|
}
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const child = exec(
|
const child = exec(
|
||||||
`${standalonePath} ${cmd}`,
|
`${standalonePath} ${cmd
|
||||||
|
.split(' ')
|
||||||
|
.filter((c) => c)
|
||||||
|
.join(' ')}`,
|
||||||
{ env: { ...process.env, ...addedEnvs } },
|
{ env: { ...process.env, ...addedEnvs } },
|
||||||
($error, $stdout, $stderr) => {
|
($error, $stdout, $stderr) => {
|
||||||
stderr = $stderr || '';
|
stderr = $stderr || '';
|
||||||
@ -417,3 +427,15 @@ export function deepJsonParse(data: any): any {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function switchSentry(
|
||||||
|
enabled: boolean | undefined,
|
||||||
|
): Promise<boolean | undefined> {
|
||||||
|
const balenaCLI = await import('../build/app');
|
||||||
|
const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions();
|
||||||
|
if (sentryOpts) {
|
||||||
|
const sentryStatus = sentryOpts.enabled;
|
||||||
|
sentryOpts.enabled = enabled;
|
||||||
|
return sentryStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -41,18 +41,23 @@ export class BuilderMock extends NockMock {
|
|||||||
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
|
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(
|
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(
|
||||||
async function (uri, requestBody) {
|
async function (uri, requestBody, callback) {
|
||||||
await opts.checkURI(uri);
|
let error: Error | null = null;
|
||||||
if (typeof requestBody === 'string') {
|
try {
|
||||||
const gzipped = Buffer.from(requestBody, 'hex');
|
await opts.checkURI(uri);
|
||||||
const gunzipped = await gunzipAsync(gzipped);
|
if (typeof requestBody === 'string') {
|
||||||
await opts.checkBuildRequestBody(gunzipped);
|
const gzipped = Buffer.from(requestBody, 'hex');
|
||||||
return [opts.responseCode, opts.responseBody];
|
const gunzipped = await gunzipAsync(gzipped);
|
||||||
} else {
|
await opts.checkBuildRequestBody(gunzipped);
|
||||||
throw new Error(
|
} else {
|
||||||
`unexpected requestBody type "${typeof requestBody}"`,
|
throw new Error(
|
||||||
);
|
`unexpected requestBody type "${typeof requestBody}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = err;
|
||||||
}
|
}
|
||||||
|
callback(error, [opts.responseCode, opts.responseBody]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -81,14 +81,21 @@ export class DockerMock extends NockMock {
|
|||||||
this.optPost(
|
this.optPost(
|
||||||
new RegExp(`^/build\\?(|.+&)${qs.stringify({ t: opts.tag })}&`),
|
new RegExp(`^/build\\?(|.+&)${qs.stringify({ t: opts.tag })}&`),
|
||||||
opts,
|
opts,
|
||||||
).reply(async function (uri, requestBody) {
|
).reply(async function (uri, requestBody, cb) {
|
||||||
await opts.checkURI(uri);
|
let error: Error | null = null;
|
||||||
if (typeof requestBody === 'string') {
|
try {
|
||||||
await opts.checkBuildRequestBody(requestBody);
|
await opts.checkURI(uri);
|
||||||
return [opts.responseCode, opts.responseBody];
|
if (typeof requestBody === 'string') {
|
||||||
} else {
|
await opts.checkBuildRequestBody(requestBody);
|
||||||
throw new Error(`unexpected requestBody type "${typeof requestBody}"`);
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`unexpected requestBody type "${typeof requestBody}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = err;
|
||||||
}
|
}
|
||||||
|
cb(error, [opts.responseCode, opts.responseBody]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"build_log": null,
|
"build_log": null,
|
||||||
"start_timestamp": "2021-08-25T22:18:33.624Z",
|
"start_timestamp": "2021-08-25T22:18:33.624Z",
|
||||||
"end_timestamp": "2021-08-25T22:18:48.820Z",
|
"end_timestamp": "2021-08-25T22:18:48.820Z",
|
||||||
"release_image": [
|
"contains__image": [
|
||||||
{
|
{
|
||||||
"image": [
|
"image": [
|
||||||
{
|
{
|
||||||
|
@ -63,7 +63,7 @@ describe('detectEncoding() function', function () {
|
|||||||
it('should correctly detect the encoding of a few selected files', async () => {
|
it('should correctly detect the encoding of a few selected files', async () => {
|
||||||
const sampleBinary = [
|
const sampleBinary = [
|
||||||
'drivelist/build/Release/drivelist.node',
|
'drivelist/build/Release/drivelist.node',
|
||||||
'mountutils/prebuilds/linux-x64/mountutils.node',
|
'mountutils/build/Release/MountUtils.node',
|
||||||
];
|
];
|
||||||
const sampleText = [
|
const sampleText = [
|
||||||
'node_modules/.bin/mocha',
|
'node_modules/.bin/mocha',
|
||||||
|
@ -42,7 +42,7 @@ describe('image-manager', function () {
|
|||||||
|
|
||||||
describe('given the image is fresh', function () {
|
describe('given the image is fresh', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
|
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
|
||||||
return this.cacheIsImageFresh.resolves(true);
|
return this.cacheIsImageFresh.resolves(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ describe('image-manager', function () {
|
|||||||
void imageManager.getStream('raspberry-pi').then(function (stream) {
|
void imageManager.getStream('raspberry-pi').then(function (stream) {
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
||||||
stream.on('data', (chunk: string) => (result += chunk.toString()));
|
stream.on('data', (chunk) => (result += chunk.toString()));
|
||||||
|
|
||||||
return stream.on('end', function () {
|
return stream.on('end', function () {
|
||||||
expect(result).to.equal('Cache image');
|
expect(result).to.equal('Cache image');
|
||||||
@ -68,7 +68,7 @@ describe('image-manager', function () {
|
|||||||
|
|
||||||
describe('given the image is not fresh', function () {
|
describe('given the image is not fresh', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
|
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
|
||||||
return this.cacheIsImageFresh.resolves(false);
|
return this.cacheIsImageFresh.resolves(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ describe('image-manager', function () {
|
|||||||
void imageManager.getStream('raspberry-pi').then((stream) => {
|
void imageManager.getStream('raspberry-pi').then((stream) => {
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
||||||
stream.on('data', (chunk: string) => (result += chunk));
|
stream.on('data', (chunk) => (result += chunk));
|
||||||
|
|
||||||
stream.on('end', async () => {
|
stream.on('end', async () => {
|
||||||
expect(result).to.equal('Download image');
|
expect(result).to.equal('Download image');
|
||||||
@ -280,7 +280,7 @@ describe('image-manager', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.isImageCached()', () => {
|
describe('.isImageFresh()', () => {
|
||||||
describe('given the raspberry-pi manifest', function () {
|
describe('given the raspberry-pi manifest', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.getDeviceTypeManifestBySlugStub = stub(
|
this.getDeviceTypeManifestBySlugStub = stub(
|
||||||
@ -314,8 +314,78 @@ describe('image-manager', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return false', async function () {
|
it('should return false', async function () {
|
||||||
expect(await imageManager.isImageCached('raspberry-pi', '1.2.3')).to
|
expect(await imageManager.isImageFresh('raspberry-pi', '1.2.3')).to.be
|
||||||
.be.false;
|
.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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -342,7 +412,7 @@ describe('image-manager', function () {
|
|||||||
.then(function (stream) {
|
.then(function (stream) {
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
||||||
stream.on('data', (chunk) => (result += chunk as string));
|
stream.on('data', (chunk: string) => (result += chunk));
|
||||||
|
|
||||||
stream.on('end', function () {
|
stream.on('end', function () {
|
||||||
expect(result).to.equal('Lorem ipsum dolor sit amet');
|
expect(result).to.equal('Lorem ipsum dolor sit amet');
|
||||||
|
Reference in New Issue
Block a user