mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
189 Commits
Author | SHA1 | Date | |
---|---|---|---|
3f9288e9d3 | |||
0efa745628 | |||
bddad252f7 | |||
a1a0e4f028 | |||
de74baa2ff | |||
6c12f755c5 | |||
f80b8e63b1 | |||
b32514f5af | |||
935f8d2549 | |||
b2de857ef1 | |||
78f1471bf4 | |||
d47abf072d | |||
8502c4db4b | |||
dd2c5c40d7 | |||
d23b253ac5 | |||
0b0e24c9b2 | |||
f9656cbe91 | |||
e174f7db4c | |||
a7a408a5c7 | |||
e5877c7de9 | |||
ecb8b3ae6b | |||
fd20516f69 | |||
5ccee0e4f1 | |||
7bb13a551c | |||
19d287aefc | |||
8d10c1af2a | |||
ebfabdba6a | |||
b0cbe43708 | |||
b7f1469912 | |||
3396ba5a97 | |||
78ffff83bc | |||
ae13d584a3 | |||
14f12d17eb | |||
45eb0ad4b1 | |||
21fd8a3307 | |||
b545bd00ad | |||
3eda7938f9 | |||
2bccabfc38 | |||
1b42d08567 | |||
55b69be987 | |||
7c8ce1b1a9 | |||
31ddacec4c | |||
dad26328b9 | |||
98197eef47 | |||
0f8ce47ec6 | |||
33d3be326a | |||
3eb397de1b | |||
457b81a597 | |||
8eb1777437 | |||
bbc08dcfc5 | |||
48cee061f4 | |||
37e96e5d67 | |||
a5c865b7f9 | |||
3fb3dd5819 | |||
daf5c518fb | |||
4fcedd0607 | |||
42d9cbb48d | |||
408efa91c1 | |||
a2209ffe56 | |||
3f27db811b | |||
839a3050fb | |||
c8ea9cfcdb | |||
776115ef5d | |||
f031ec1dea | |||
fe42438090 | |||
b616fbdd79 | |||
81edfbbae1 | |||
663e83c3b8 | |||
b650f8ff6d | |||
58234f17e1 | |||
77905f4a74 | |||
30076fabe6 | |||
28703bb5ae | |||
37b3c6abe9 | |||
b4e473e4d4 | |||
0d4e411777 | |||
7e6f2189e8 | |||
3903daf8a8 | |||
18bc0d61e7 | |||
7f2daeebb0 | |||
813e9cb82e | |||
3bcb3c1b2e | |||
20d76556c2 | |||
e829068725 | |||
650e896f70 | |||
a9042124ea | |||
d24d78dac7 | |||
42c50ef8ae | |||
ba4b9bd447 | |||
02c0ea5b59 | |||
bc3558dd8e | |||
aad62d1ccd | |||
ecc6f80164 | |||
c0fd1e3886 | |||
9d3120b144 | |||
ed0e03ddb2 | |||
8fe6d6c026 | |||
727033ae14 | |||
c19ce6a905 | |||
1a33029738 | |||
043bc48a1c | |||
a10156a441 | |||
4f665f43d2 | |||
9f097a96f5 | |||
64d1943804 | |||
666ce876e6 | |||
e01184080f | |||
93039b010d | |||
795259bf30 | |||
fa134d2d39 | |||
bef5221ed8 | |||
72d6db796c | |||
e848eb63ee | |||
6f0f7350cf | |||
07a88c700e | |||
9cae66bd92 | |||
cddea24cef | |||
b1c246c0b4 | |||
00b4d57a03 | |||
2cba82e914 | |||
1352c5c823 | |||
c86eb97010 | |||
53be743b9d | |||
d9f21b4c3f | |||
261ab398dd | |||
f28a9992e4 | |||
29e7827eb1 | |||
1d77cf3665 | |||
017c767f61 | |||
7d79c4e24b | |||
60bc5092e0 | |||
a33a794931 | |||
f0ede6fca2 | |||
dbe177e766 | |||
09f80730a8 | |||
327d28c103 | |||
56ab785a82 | |||
305d65d5ed | |||
c4d3686a34 | |||
ce06854b55 | |||
8db05cc8a7 | |||
7a22c987d2 | |||
45efbcdfe3 | |||
d6a9b78b3e | |||
e8ac3ea960 | |||
0ffa0f85a2 | |||
5e7479f60e | |||
07365c45f2 | |||
e5076434c6 | |||
5d687f5a55 | |||
e192767156 | |||
5a8d2fad5f | |||
45f482fad1 | |||
c0e7ae9c91 | |||
36077cacda | |||
82b9983450 | |||
703dbd01c9 | |||
602e63c8a9 | |||
2ab635f49a | |||
322736a145 | |||
c347b67b25 | |||
4022beeb56 | |||
ccf97cfc9f | |||
9c5fe14f2e | |||
38e29251e7 | |||
bfc7a14646 | |||
610db81fcb | |||
d1f7d6d07f | |||
694eb78aaa | |||
1caccafbcd | |||
61d4d1f1e7 | |||
a01c85bc15 | |||
5d7b7cfc6f | |||
92fd9e0883 | |||
24273b5ac0 | |||
6155509f4c | |||
735af9f6a9 | |||
d7c60e6dea | |||
bcb42c8a21 | |||
04f5e0fa2b | |||
8cb5804848 | |||
91c3fced49 | |||
99a94eafbb | |||
b4cff78588 | |||
8577bb6281 | |||
e0f081623b | |||
2887ab8200 | |||
aaf4625abb | |||
6f30dc0550 |
@ -1,2 +0,0 @@
|
|||||||
/completion/*
|
|
||||||
/bin/*
|
|
21
.eslintrc.js
21
.eslintrc.js
@ -1,21 +0,0 @@
|
|||||||
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 zip package or executable installer
|
- **Install method:** npm or standalone 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
|
||||||
|
18
.github/actions/publish/action.yml
vendored
18
.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: '20.x'
|
default: '22.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@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
||||||
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@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # 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,9 +135,11 @@ 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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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: dist
|
path: |
|
||||||
|
dist
|
||||||
|
!dist/balena
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
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: '20.x'
|
default: '22.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@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # 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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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
|
||||||
|
4
.github/workflows/flowzone.yml
vendored
4
.github/workflows/flowzone.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
|||||||
"os": [
|
"os": [
|
||||||
["self-hosted", "X64"],
|
["self-hosted", "X64"],
|
||||||
["self-hosted", "ARM64"],
|
["self-hosted", "ARM64"],
|
||||||
["macos-12"],
|
["macos-13"],
|
||||||
["windows-2019"],
|
["windows-2019"],
|
||||||
["macos-latest-xlarge"]
|
["macos-latest-xlarge"]
|
||||||
]
|
]
|
||||||
@ -36,7 +36,7 @@ jobs:
|
|||||||
"os": [
|
"os": [
|
||||||
["self-hosted", "X64"],
|
["self-hosted", "X64"],
|
||||||
["self-hosted", "ARM64"],
|
["self-hosted", "ARM64"],
|
||||||
["macos-12"],
|
["macos-13"],
|
||||||
["windows-2019"],
|
["windows-2019"],
|
||||||
["macos-latest-xlarge"]
|
["macos-latest-xlarge"]
|
||||||
]
|
]
|
||||||
|
@ -2,7 +2,7 @@ module.exports = {
|
|||||||
reporter: 'spec',
|
reporter: 'spec',
|
||||||
require: 'ts-node/register/transpile-only',
|
require: 'ts-node/register/transpile-only',
|
||||||
file: './tests/config-tests',
|
file: './tests/config-tests',
|
||||||
timeout: 12000,
|
timeout: 48000,
|
||||||
// To test only, say, 'push.spec.ts', do it as follows so that
|
// To test only, say, 'push.spec.ts', do it as follows so that
|
||||||
// requests are authenticated:
|
// requests are authenticated:
|
||||||
// spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],
|
// spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],
|
||||||
|
File diff suppressed because it is too large
Load Diff
460
CHANGELOG.md
460
CHANGELOG.md
@ -4,6 +4,466 @@ 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
|
||||||
|
|
||||||
|
* Add more realistic os configure tests [Thodoris Greasidis]
|
||||||
|
|
||||||
|
## 20.1.5 - 2024-12-20
|
||||||
|
|
||||||
|
* Update shrinkwrapped express to v4.21.2 [Oskar Williams]
|
||||||
|
|
||||||
|
## 20.1.4 - 2024-12-20
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> Update balena-device-init to 8.0.0 [Thodoris Greasidis] </summary>
|
||||||
|
|
||||||
|
> ### balena-device-init-8.0.0 - 2024-12-18
|
||||||
|
>
|
||||||
|
> * Avoid running linting in the custom tests [Thodoris Greasidis]
|
||||||
|
> * Stop returning Bluebird promises [Thodoris Greasidis]
|
||||||
|
> * package: Publish only the build & typings folders [Thodoris Greasidis]
|
||||||
|
> * Convert to TypeScript with es6 module notation [Thodoris Greasidis]
|
||||||
|
> * Replace gulp, coffeelint & mochainon with tsc, @balena/lint, mocha, chai & sinon [Thodoris Greasidis]
|
||||||
|
> * Drop support for node <20.6.0 [Thodoris Greasidis]
|
||||||
|
>
|
||||||
|
> ### balena-device-init-7.0.2 - 2024-12-17
|
||||||
|
>
|
||||||
|
> * flowzone: Update runner versions [Thodoris Greasidis]
|
||||||
|
> * Pin etcher-sdk to 9.0.8 to match resin-device-operations and fix tests [Thodoris Greasidis]
|
||||||
|
>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 20.1.3 - 2024-12-20
|
||||||
|
|
||||||
|
* Update oclif to 4.17.0 and @oclif/core 4.1.0 [Otavio Jacobi]
|
||||||
|
|
||||||
|
## 20.1.2 - 2024-12-17
|
||||||
|
|
||||||
|
* Remove unnecessary `Promise.resolve` and `Promise.reject` [Pagan Gazzard]
|
||||||
|
|
||||||
|
## 20.1.1 - 2024-12-16
|
||||||
|
|
||||||
|
* Update @balena/lint to v9.1.3 [Otavio Jacobi]
|
||||||
|
|
||||||
|
## 20.1.0 - 2024-12-12
|
||||||
|
|
||||||
|
* `device os-update`: Add handling for updates that require takeover [myarmolinsky]
|
||||||
|
* Update `balena-sdk` [myarmolinsky]
|
||||||
|
* Update `@balena/compose` [myarmolinsky]
|
||||||
|
|
||||||
|
## 20.0.9 - 2024-12-05
|
||||||
|
|
||||||
|
* Update shrinkwrapped express to v4.21.1 [Oskar Williams]
|
||||||
|
|
||||||
|
## 20.0.8 - 2024-12-04
|
||||||
|
|
||||||
|
* Run test and publish with macos-13 [Otavio Jacobi]
|
||||||
|
|
||||||
|
## 20.0.7 - 2024-11-23
|
||||||
|
|
||||||
|
* Update TypeScript to 5.7.2 [Thodoris Greasidis]
|
||||||
|
|
||||||
|
## 20.0.6 - 2024-11-08
|
||||||
|
|
||||||
|
* Refactor balena build for clarity [Thodoris Greasidis]
|
||||||
|
|
||||||
|
## 20.0.5 - 2024-11-05
|
||||||
|
|
||||||
|
* Update actions/upload-artifact digest to b4b15b8 [balena-renovate[bot]]
|
||||||
|
|
||||||
|
## 20.0.4 - 2024-11-05
|
||||||
|
|
||||||
|
* Update actions/setup-node digest to 39370e3 [balena-renovate[bot]]
|
||||||
|
|
||||||
|
## 20.0.3 - 2024-11-05
|
||||||
|
|
||||||
|
* api-key generate: Display a descriptive error when the generation fails due to a stale JWT [Thodoris Greasidis]
|
||||||
|
|
||||||
|
## 20.0.2 - 2024-10-29
|
||||||
|
|
||||||
|
* Restore ability to cat key into `ssh-key add` [myarmolinsky]
|
||||||
|
|
||||||
## 20.0.1 - 2024-10-29
|
## 20.0.1 - 2024-10-29
|
||||||
|
|
||||||
* Fix sending input to some aliases not working [myarmolinsky]
|
* Fix sending input to some aliases not working [myarmolinsky]
|
||||||
|
@ -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 zip packages (which is rather slow)
|
* `npm run test:source` skips testing the standalone 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 Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
|
* [Standalone tar.gz Package](#standalone-targz-package): these are plain tar.gz files with the balena CLI
|
||||||
executable in them: extract and run. Available for all platforms: Linux, Windows, macOS.
|
bundled within. 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 zip package, it may be a good idea to
|
If you had previously installed the CLI using a standalone tar 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 Zip Package](#standalone-zip-package) instructions
|
entry that comes first. Check the [Standalone tar.gz Package](#standalone-targz-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,18 +42,17 @@ 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 Zip Package
|
## Standalone tar.gz Package
|
||||||
|
|
||||||
1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
1. Download the latest tar.gz 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.zip` ← _also for the Windows Subsystem for Linux_
|
`balena-cli-vX.Y.Z-linux-x64-standalone.tar.gz` ← _also for the Windows Subsystem for Linux_
|
||||||
`balena-cli-vX.Y.Z-macOS-x64-standalone.zip`
|
`balena-cli-vX.Y.Z-macOS-x64-standalone.tar.gz`
|
||||||
`balena-cli-vX.Y.Z-windows-x64-standalone.zip`
|
`balena-cli-vX.Y.Z-windows-x64-standalone.tar.gz`
|
||||||
|
|
||||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
2. Extract the tar.gz file contents to any folder you choose. The extracted contents will be a `balena` folder containing a `bin` subdirectory.
|
||||||
`balena-cli` folder.
|
|
||||||
|
|
||||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable.
|
3. Add the `balena/bin` 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) |
|
||||||
@ -61,14 +60,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 zip package is not currently compatible with
|
> * **Linux Alpine** and **Busybox:** the standalone tar.gz 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` executable out of the extracted `balena-cli` folder on its own
|
> * Note that moving the `balena/bin/balena` executable out of the extracted `balena` 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-cli` folder.
|
> folders and files also present in the `balena` folder.
|
||||||
|
|
||||||
To update the CLI to a new version, download a new release zip file and replace the previous
|
To update the CLI to a new version, download a new release tar.gz 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.
|
||||||
|
|
||||||
@ -78,8 +77,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 21 and later are not yet fully supported.**
|
> **Versions 23 and later are not yet fully supported.**
|
||||||
|
|
||||||
### Install development tools
|
### Install development tools
|
||||||
|
|
||||||
@ -89,7 +88,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 20
|
$ nvm install 22
|
||||||
```
|
```
|
||||||
|
|
||||||
The `curl` command line above uses
|
The `curl` command line above uses
|
||||||
@ -106,7 +105,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 20
|
$ nvm install 22
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **Windows** (not WSL)
|
#### **Windows** (not WSL)
|
||||||
@ -114,7 +113,7 @@ $ nvm install 20
|
|||||||
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 v20 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
- Node.js v22 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:
|
||||||
|
@ -8,15 +8,15 @@ method.
|
|||||||
|
|
||||||
Selected operating system: **Linux**
|
Selected operating system: **Linux**
|
||||||
|
|
||||||
1. Download the latest zip file from the [latest release
|
1. Download the latest tar.gz file from the [latest release
|
||||||
page](https://github.com/balena-io/balena-cli/releases/latest). Look for a file name that ends
|
page](https://github.com/balena-io/balena-cli/releases/latest). Look for a file name that ends
|
||||||
with "-standalone.zip", for example:
|
with "-standalone.tar.gz", for example:
|
||||||
`balena-cli-vX.Y.Z-linux-x64-standalone.zip`
|
`balena-cli-vX.Y.Z-linux-x64-standalone.tar.gz`
|
||||||
|
|
||||||
2. Extract the zip file contents to any folder you choose, for example `/home/james`.
|
2. Extract the tar.gz file contents to any folder you choose, for example `/home/james`.
|
||||||
The extracted contents will include a `balena-cli` folder.
|
The extracted contents will include a `balena/bin` folder.
|
||||||
|
|
||||||
3. Add that folder (e.g. `/home/james/balena-cli`) to the `PATH` environment variable.
|
3. Add that folder (e.g. `/home/james/balena/bin`) to the `PATH` environment variable.
|
||||||
Check this [StackOverflow
|
Check this [StackOverflow
|
||||||
post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)
|
post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)
|
||||||
for instructions. Close and reopen the terminal window so that the changes to `PATH`
|
for instructions. Close and reopen the terminal window so that the changes to `PATH`
|
||||||
@ -27,7 +27,7 @@ Selected operating system: **Linux**
|
|||||||
* `balena version` - should print the CLI's version
|
* `balena version` - should print the CLI's version
|
||||||
* `balena help` - should print a list of available commands
|
* `balena help` - should print a list of available commands
|
||||||
|
|
||||||
To update the balena CLI to a new version, download a new release zip file and replace the previous
|
To update the balena CLI to a new version, download a new release tar.gz 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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
45
MIGRATIONS.md
Normal file
45
MIGRATIONS.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
## 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,6 +20,8 @@ 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
|
||||||
zip package for Linux. However, commands like "balena build" will, by default, attempt to reach the
|
tar.gz 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:
|
||||||
|
@ -15,29 +15,17 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { JsonVersions } from '../src/commands/version/index';
|
|
||||||
|
|
||||||
import { run as oclifRun } from '@oclif/core';
|
import { run as oclifRun } from '@oclif/core';
|
||||||
import * as archiver from 'archiver';
|
|
||||||
import { exec, execFile } from 'child_process';
|
import { exec, execFile } from 'child_process';
|
||||||
import * as filehound from 'filehound';
|
|
||||||
import type { Stats } from 'fs';
|
import type { Stats } from 'fs';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as klaw from 'klaw';
|
import * as klaw from 'klaw';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as rimraf from 'rimraf';
|
import * as rimraf from 'rimraf';
|
||||||
import * as semver from 'semver';
|
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { notarize } from '@electron/notarize';
|
import { notarize } from '@electron/notarize';
|
||||||
|
|
||||||
import { stripIndent } from '../build/utils/lazy';
|
import { loadPackageJson, ROOT, whichSpawn } from './utils';
|
||||||
import {
|
|
||||||
diffLines,
|
|
||||||
loadPackageJson,
|
|
||||||
ROOT,
|
|
||||||
StdOutTap,
|
|
||||||
whichSpawn,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@ -55,12 +43,6 @@ interface PathByPlatform {
|
|||||||
[platform: string]: string;
|
[platform: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const standaloneZips: PathByPlatform = {
|
|
||||||
linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.zip`),
|
|
||||||
darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.zip`),
|
|
||||||
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOclifInstallersOriginalNames = async (): Promise<PathByPlatform> => {
|
const getOclifInstallersOriginalNames = async (): Promise<PathByPlatform> => {
|
||||||
const { stdout } = await execAsync('git rev-parse --short HEAD');
|
const { stdout } = await execAsync('git rev-parse --short HEAD');
|
||||||
const sha = stdout.trim();
|
const sha = stdout.trim();
|
||||||
@ -75,260 +57,28 @@ const renamedOclifInstallers: PathByPlatform = {
|
|||||||
win32: dPath(`balena-cli-${version}-windows-${arch}-installer.exe`),
|
win32: dPath(`balena-cli-${version}-windows-${arch}-installer.exe`),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const finalReleaseAssets: { [platform: string]: string[] } = {
|
const getOclifStandaloneOriginalNames = async (): Promise<PathByPlatform> => {
|
||||||
win32: [standaloneZips['win32'], renamedOclifInstallers['win32']],
|
const { stdout } = await execAsync('git rev-parse --short HEAD');
|
||||||
darwin: [standaloneZips['darwin'], renamedOclifInstallers['darwin']],
|
const sha = stdout.trim();
|
||||||
linux: [standaloneZips['linux']],
|
return {
|
||||||
|
linux: dPath(`balena-${version}-${sha}-linux-${arch}.tar.gz`),
|
||||||
|
darwin: dPath(`balena-${version}-${sha}-darwin-${arch}.tar.gz`),
|
||||||
|
win32: dPath(`balena-${version}-${sha}-win32-${arch}.tar.gz`),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const renamedOclifStandalone: PathByPlatform = {
|
||||||
* Given the output of `pkg` as a string (containing warning messages),
|
linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.tar.gz`),
|
||||||
* diff it against previously saved output of known "safe" warnings.
|
darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.tar.gz`),
|
||||||
* Throw an error if the diff is not empty.
|
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.tar.gz`),
|
||||||
*/
|
};
|
||||||
async function diffPkgOutput(pkgOut: string) {
|
|
||||||
const { monochrome } = await import('../tests/helpers');
|
|
||||||
const relSavedPath = path.join(
|
|
||||||
'tests',
|
|
||||||
'test-data',
|
|
||||||
'pkg',
|
|
||||||
`expected-warnings-${process.platform}-${arch}.txt`,
|
|
||||||
);
|
|
||||||
const absSavedPath = path.join(ROOT, relSavedPath);
|
|
||||||
const ignoreStartsWith = [
|
|
||||||
'> pkg@',
|
|
||||||
'> Fetching base Node.js binaries',
|
|
||||||
' fetched-',
|
|
||||||
'prebuild-install WARN install No prebuilt binaries found',
|
|
||||||
];
|
|
||||||
const modulesRE =
|
|
||||||
process.platform === 'win32'
|
|
||||||
? /(?<=[ '])([A-Z]:)?\\.+?\\node_modules(?=\\)/
|
|
||||||
: /(?<=[ '])\/.+?\/node_modules(?=\/)/;
|
|
||||||
const buildRE =
|
|
||||||
process.platform === 'win32'
|
|
||||||
? /(?<=[ '])([A-Z]:)?\\.+\\build(?=\\)/
|
|
||||||
: /(?<=[ '])\/.+\/build(?=\/)/;
|
|
||||||
|
|
||||||
const cleanLines = (chunks: string | string[]) => {
|
|
||||||
const lines = typeof chunks === 'string' ? chunks.split('\n') : chunks;
|
|
||||||
return lines
|
|
||||||
.map((line: string) => monochrome(line)) // remove ASCII colors
|
|
||||||
.filter((line: string) => !/^\s*$/.test(line)) // blank lines
|
|
||||||
.filter((line: string) =>
|
|
||||||
ignoreStartsWith.every((i) => !line.startsWith(i)),
|
|
||||||
)
|
|
||||||
.map((line: string) => {
|
|
||||||
// replace absolute paths with relative paths
|
|
||||||
let replaced = line.replace(modulesRE, 'node_modules');
|
|
||||||
if (replaced === line) {
|
|
||||||
replaced = line.replace(buildRE, 'build');
|
|
||||||
}
|
|
||||||
return replaced;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
pkgOut = cleanLines(pkgOut).join('\n');
|
|
||||||
const { readFile } = (await import('fs')).promises;
|
|
||||||
const expectedOut = cleanLines(await readFile(absSavedPath, 'utf8')).join(
|
|
||||||
'\n',
|
|
||||||
);
|
|
||||||
if (expectedOut !== pkgOut) {
|
|
||||||
const sep =
|
|
||||||
'================================================================================';
|
|
||||||
const diff = diffLines(expectedOut, pkgOut);
|
|
||||||
const msg = `pkg output does not match expected output from "${relSavedPath}"
|
|
||||||
Diff:
|
|
||||||
${sep}
|
|
||||||
${diff}
|
|
||||||
${sep}
|
|
||||||
Check whether the new or changed pkg warnings are safe to ignore, then update
|
|
||||||
"${relSavedPath}"
|
|
||||||
and share the result of your investigation as comments on the pull request.
|
|
||||||
Hint: the fix is often a matter of updating the 'pkg.scripts' or 'pkg.assets'
|
|
||||||
sections in the CLI's 'package.json' file, or a matter of updating the
|
|
||||||
'buildPkg' function in 'automation/build-bin.ts'. Sometimes it requires
|
|
||||||
patching dependencies: See for example 'patches/all/open+7.0.2.patch'.
|
|
||||||
${sep}
|
|
||||||
`;
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call `pkg.exec` to generate the standalone zip file, capturing its warning
|
|
||||||
* messages (stdout and stderr) in order to call diffPkgOutput().
|
|
||||||
*/
|
|
||||||
async function execPkg(...args: any[]) {
|
|
||||||
const { exec: pkgExec } = await import('@yao-pkg/pkg');
|
|
||||||
const outTap = new StdOutTap(true);
|
|
||||||
try {
|
|
||||||
outTap.tap();
|
|
||||||
await (pkgExec as any)(...args);
|
|
||||||
} catch (err) {
|
|
||||||
outTap.untap();
|
|
||||||
console.log(outTap.stdoutBuf.join(''));
|
|
||||||
console.error(outTap.stderrBuf.join(''));
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
outTap.untap();
|
|
||||||
await diffPkgOutput(outTap.allBuf.join(''));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use the 'pkg' module to create a single large executable file with
|
|
||||||
* the contents of 'node_modules' and the CLI's javascript code.
|
|
||||||
* Also copy a number of native modules (binary '.node' files) that are
|
|
||||||
* compiled during 'npm install' to the 'build-bin' folder, alongside
|
|
||||||
* the single large executable file created by pkg. (This is necessary
|
|
||||||
* because of a pkg limitation that does not allow binary executables
|
|
||||||
* to be directly executed from inside another binary executable.)
|
|
||||||
*/
|
|
||||||
async function buildPkg() {
|
|
||||||
// https://github.com/vercel/pkg#targets
|
|
||||||
let targets = `linux-${arch}`;
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
targets = `macos-${arch}`;
|
|
||||||
}
|
|
||||||
// TBC: not yet possible to build for Windows arm64 on x64 nodes
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
targets = `win-x64`;
|
|
||||||
}
|
|
||||||
const args = [
|
|
||||||
'--targets',
|
|
||||||
targets,
|
|
||||||
'--output',
|
|
||||||
'build-bin/balena',
|
|
||||||
'package.json',
|
|
||||||
];
|
|
||||||
console.log('=======================================================');
|
|
||||||
console.log(`execPkg ${args.join(' ')}`);
|
|
||||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
|
||||||
console.log('=======================================================');
|
|
||||||
|
|
||||||
await execPkg(args);
|
|
||||||
|
|
||||||
const paths: Array<[string, string[], string[]]> = [
|
|
||||||
// [platform, [source path], [destination path]]
|
|
||||||
['*', ['open', 'xdg-open'], ['xdg-open']],
|
|
||||||
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
|
||||||
];
|
|
||||||
await Promise.all(
|
|
||||||
paths.map(([platform, source, dest]) => {
|
|
||||||
if (platform === '*' || platform === process.platform) {
|
|
||||||
// eg copy from node_modules/open/xdg-open to build-bin/xdg-open
|
|
||||||
return fs.copy(
|
|
||||||
path.join(ROOT, 'node_modules', ...source),
|
|
||||||
path.join(ROOT, 'build-bin', ...dest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const nativeExtensionPaths: string[] = await filehound
|
|
||||||
.create()
|
|
||||||
.paths(path.join(ROOT, 'node_modules'))
|
|
||||||
.ext(['node', 'dll'])
|
|
||||||
.find();
|
|
||||||
|
|
||||||
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
nativeExtensionPaths.map((extPath) =>
|
|
||||||
fs.copy(
|
|
||||||
extPath,
|
|
||||||
extPath.replace(
|
|
||||||
path.join(ROOT, 'node_modules'),
|
|
||||||
path.join(ROOT, 'build-bin'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run some basic tests on the built pkg executable.
|
|
||||||
* TODO: test more than just `balena version -j`; integrate with the
|
|
||||||
* existing mocha/chai CLI command testing.
|
|
||||||
*/
|
|
||||||
async function testPkg() {
|
|
||||||
const pkgBalenaPath = path.join(
|
|
||||||
ROOT,
|
|
||||||
'build-bin',
|
|
||||||
process.platform === 'win32' ? 'balena.exe' : 'balena',
|
|
||||||
);
|
|
||||||
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
|
|
||||||
// Run `balena version -j`, parse its stdout as JSON, and check that the
|
|
||||||
// reported Node.js major version matches semver.major(process.version)
|
|
||||||
let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
|
|
||||||
'version',
|
|
||||||
'-j',
|
|
||||||
]);
|
|
||||||
const { filterCliOutputForTests } = await import('../tests/helpers');
|
|
||||||
const filtered = filterCliOutputForTests({
|
|
||||||
err: stderr.split(/\r?\n/),
|
|
||||||
out: stdout.split(/\r?\n/),
|
|
||||||
});
|
|
||||||
stdout = filtered.out.join('\n');
|
|
||||||
stderr = filtered.err.join('\n');
|
|
||||||
let pkgNodeVersion = '';
|
|
||||||
let pkgNodeMajorVersion = 0;
|
|
||||||
try {
|
|
||||||
const balenaVersions: JsonVersions = JSON.parse(stdout);
|
|
||||||
pkgNodeVersion = balenaVersions['Node.js'];
|
|
||||||
pkgNodeMajorVersion = semver.major(pkgNodeVersion);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(stripIndent`
|
|
||||||
Error parsing JSON output of "balena version -j": ${err}
|
|
||||||
Original output: "${stdout}"`);
|
|
||||||
}
|
|
||||||
if (semver.major(process.version) !== pkgNodeMajorVersion) {
|
|
||||||
throw new Error(
|
|
||||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (filtered.err.length > 0) {
|
|
||||||
const err = filtered.err.join('\n');
|
|
||||||
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
|
|
||||||
}
|
|
||||||
console.log('Success! (standalone package test successful)');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the zip file for the standalone 'pkg' bundle previously created
|
|
||||||
* by the buildPkg() function in 'build-bin.ts'.
|
|
||||||
*/
|
|
||||||
async function zipPkg() {
|
|
||||||
const outputFile = standaloneZips[process.platform];
|
|
||||||
if (!outputFile) {
|
|
||||||
throw new Error(
|
|
||||||
`Standalone installer unavailable for platform "${process.platform}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await fs.mkdirp(path.dirname(outputFile));
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
console.log(`Zipping standalone package to "${outputFile}"...`);
|
|
||||||
|
|
||||||
const archive = archiver('zip', {
|
|
||||||
zlib: { level: 7 },
|
|
||||||
});
|
|
||||||
archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli');
|
|
||||||
|
|
||||||
const outputStream = fs.createWriteStream(outputFile);
|
|
||||||
|
|
||||||
outputStream.on('close', resolve);
|
|
||||||
outputStream.on('error', reject);
|
|
||||||
|
|
||||||
archive.on('error', reject);
|
|
||||||
archive.on('warning', console.warn);
|
|
||||||
|
|
||||||
archive.pipe(outputStream);
|
|
||||||
archive.finalize().catch(reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function signFilesForNotarization() {
|
export async function signFilesForNotarization() {
|
||||||
console.log('Signing files for notarization');
|
console.log('Signing files for notarization');
|
||||||
if (process.platform !== 'darwin') {
|
// 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) {
|
||||||
|
console.log('Skipping signing for notarization');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Deleting unneeded zip files...');
|
console.log('Deleting unneeded zip files...');
|
||||||
@ -416,20 +166,39 @@ export async function signFilesForNotarization() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildStandaloneZip() {
|
export async function buildStandalone() {
|
||||||
console.log(`Building standalone zip package for CLI ${version}`);
|
console.log(`Building standalone tarball for CLI ${version}`);
|
||||||
|
fs.rmSync('./tmp', { recursive: true, force: true });
|
||||||
|
fs.rmSync('./dist', { recursive: true, force: true });
|
||||||
|
fs.mkdirSync('./dist');
|
||||||
try {
|
try {
|
||||||
await buildPkg();
|
let packOpts = ['-r', ROOT, '--no-xz'];
|
||||||
await testPkg();
|
if (process.platform === 'darwin') {
|
||||||
await zipPkg();
|
packOpts = packOpts.concat('--targets', `darwin-${arch}`);
|
||||||
console.log(`Standalone zip package build completed`);
|
} else if (process.platform === 'win32') {
|
||||||
|
packOpts = packOpts.concat('--targets', 'win32-x64');
|
||||||
|
} else if (process.platform === 'linux') {
|
||||||
|
packOpts = packOpts.concat('--targets', `linux-${arch}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Building oclif installer for CLI ${version}`);
|
||||||
|
const packCmd = `pack:tarballs`;
|
||||||
|
console.log('=======================================================');
|
||||||
|
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
|
||||||
|
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||||
|
console.log('=======================================================');
|
||||||
|
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
|
||||||
|
await oclifRun([packCmd].concat(...packOpts), oclifPath);
|
||||||
|
await renameStandalone();
|
||||||
|
|
||||||
|
console.log(`Standalone tarball package build completed`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error creating or testing standalone zip package`);
|
console.error(`Error creating or testing standalone tarball package`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameInstallerFiles() {
|
async function renameInstallers() {
|
||||||
const oclifInstallers = await getOclifInstallersOriginalNames();
|
const oclifInstallers = await getOclifInstallersOriginalNames();
|
||||||
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
||||||
await fs.rename(
|
await fs.rename(
|
||||||
@ -439,6 +208,16 @@ async function renameInstallerFiles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renameStandalone() {
|
||||||
|
const oclifStandalone = await getOclifStandaloneOriginalNames();
|
||||||
|
if (await fs.pathExists(oclifStandalone[process.platform])) {
|
||||||
|
await fs.rename(
|
||||||
|
oclifStandalone[process.platform],
|
||||||
|
renamedOclifStandalone[process.platform],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
||||||
* executable installer using Microsoft SignTool.exe (Sign Tool)
|
* executable installer using Microsoft SignTool.exe (Sign Tool)
|
||||||
@ -446,7 +225,7 @@ async function renameInstallerFiles() {
|
|||||||
*/
|
*/
|
||||||
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 = renamedOclifInstallers[process.platform];
|
const exeName = (await getOclifInstallersOriginalNames())[process.platform];
|
||||||
console.log(`Signing installer "${exeName}"`);
|
console.log(`Signing installer "${exeName}"`);
|
||||||
// trust ...
|
// trust ...
|
||||||
await execFileAsync('signtool.exe', [
|
await execFileAsync('signtool.exe', [
|
||||||
@ -480,12 +259,14 @@ 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: renamedOclifInstallers.darwin,
|
appPath,
|
||||||
appleId,
|
appleId,
|
||||||
appleIdPassword,
|
appleIdPassword,
|
||||||
});
|
});
|
||||||
@ -525,7 +306,6 @@ 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 renameInstallerFiles();
|
|
||||||
// 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
|
||||||
@ -537,6 +317,7 @@ 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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -572,4 +353,5 @@ 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();
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
|
|||||||
throw new Error(`Error parsing section title`);
|
throw new Error(`Error parsing section title`);
|
||||||
}
|
}
|
||||||
// match[1] has the title, match[2] has the rest
|
// match[1] has the title, match[2] has the rest
|
||||||
return match && match[2];
|
return match?.[2];
|
||||||
}),
|
}),
|
||||||
mdParser.getSectionOfTitle('Installation'),
|
mdParser.getSectionOfTitle('Installation'),
|
||||||
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),
|
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),
|
||||||
|
@ -19,7 +19,7 @@ import * as _ from 'lodash';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
buildOclifInstaller,
|
buildOclifInstaller,
|
||||||
buildStandaloneZip,
|
buildStandalone,
|
||||||
catchUncommitted,
|
catchUncommitted,
|
||||||
signFilesForNotarization,
|
signFilesForNotarization,
|
||||||
testShrinkwrap,
|
testShrinkwrap,
|
||||||
@ -36,7 +36,7 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
|||||||
* Trivial command-line parser. Check whether the command-line argument is one
|
* Trivial command-line parser. Check whether the command-line argument is one
|
||||||
* of the following strings, then call the appropriate functions:
|
* of the following strings, then call the appropriate functions:
|
||||||
* 'build:installer' (to build a native oclif installer)
|
* 'build:installer' (to build a native oclif installer)
|
||||||
* 'build:standalone' (to build a standalone pkg package)
|
* 'build:standalone' (to build a standalone package)
|
||||||
*
|
*
|
||||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||||
*/
|
*/
|
||||||
@ -49,7 +49,7 @@ async function parse(args?: string[]) {
|
|||||||
}
|
}
|
||||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||||
'build:installer': buildOclifInstaller,
|
'build:installer': buildOclifInstaller,
|
||||||
'build:standalone': buildStandaloneZip,
|
'build:standalone': buildStandalone,
|
||||||
'sign:binaries': signFilesForNotarization,
|
'sign:binaries': signFilesForNotarization,
|
||||||
'catch-uncommitted': catchUncommitted,
|
'catch-uncommitted': catchUncommitted,
|
||||||
'test-shrinkwrap': testShrinkwrap,
|
'test-shrinkwrap': testShrinkwrap,
|
||||||
|
@ -3,7 +3,7 @@ import * as semver from 'semver';
|
|||||||
|
|
||||||
const changeTypes = ['major', 'minor', 'patch'] as const;
|
const changeTypes = ['major', 'minor', 'patch'] as const;
|
||||||
|
|
||||||
const validateChangeType = (maybeChangeType: string = 'minor') => {
|
const validateChangeType = (maybeChangeType = 'minor') => {
|
||||||
maybeChangeType = maybeChangeType.toLowerCase();
|
maybeChangeType = maybeChangeType.toLowerCase();
|
||||||
switch (maybeChangeType) {
|
switch (maybeChangeType) {
|
||||||
case 'patch':
|
case 'patch':
|
||||||
@ -136,5 +136,4 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void main();
|
||||||
main();
|
|
||||||
|
@ -18,72 +18,10 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { diffTrimmedLines } from 'diff';
|
import * as whichMod from 'which';
|
||||||
|
|
||||||
export const ROOT = path.join(__dirname, '..');
|
export const ROOT = path.join(__dirname, '..');
|
||||||
|
|
||||||
/** Tap and buffer this process' stdout and stderr */
|
|
||||||
export class StdOutTap {
|
|
||||||
public stdoutBuf: string[] = [];
|
|
||||||
public stderrBuf: string[] = [];
|
|
||||||
public allBuf: string[] = []; // both stdout and stderr
|
|
||||||
|
|
||||||
protected origStdoutWrite: typeof process.stdout.write;
|
|
||||||
protected origStderrWrite: typeof process.stdout.write;
|
|
||||||
|
|
||||||
constructor(protected printDots = false) {}
|
|
||||||
|
|
||||||
tap() {
|
|
||||||
this.origStdoutWrite = process.stdout.write;
|
|
||||||
this.origStderrWrite = process.stderr.write;
|
|
||||||
|
|
||||||
process.stdout.write = (chunk: string, ...args: any[]): boolean => {
|
|
||||||
this.stdoutBuf.push(chunk);
|
|
||||||
this.allBuf.push(chunk);
|
|
||||||
const str = this.printDots ? '.' : chunk;
|
|
||||||
return this.origStdoutWrite.call(process.stdout, str, ...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
process.stderr.write = (chunk: string, ...args: any[]): boolean => {
|
|
||||||
this.stderrBuf.push(chunk);
|
|
||||||
this.allBuf.push(chunk);
|
|
||||||
const str = this.printDots ? '.' : chunk;
|
|
||||||
return this.origStderrWrite.call(process.stderr, str, ...args);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
untap() {
|
|
||||||
process.stdout.write = this.origStdoutWrite;
|
|
||||||
process.stderr.write = this.origStderrWrite;
|
|
||||||
if (this.printDots) {
|
|
||||||
console.error('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Diff strings by line, using the 'diff' npm package:
|
|
||||||
* https://www.npmjs.com/package/diff
|
|
||||||
*/
|
|
||||||
export function diffLines(str1: string, str2: string): string {
|
|
||||||
const diffObjs = diffTrimmedLines(str1, str2);
|
|
||||||
const prefix = (chunk: string, char: string) =>
|
|
||||||
chunk
|
|
||||||
.split('\n')
|
|
||||||
.map((line: string) => `${char} ${line}`)
|
|
||||||
.join('\n');
|
|
||||||
const diffStr = diffObjs
|
|
||||||
.map((part: any) => {
|
|
||||||
return part.added
|
|
||||||
? prefix(part.value, '+')
|
|
||||||
: part.removed
|
|
||||||
? prefix(part.value, '-')
|
|
||||||
: prefix(part.value, ' ');
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
return diffStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadPackageJson() {
|
export function loadPackageJson() {
|
||||||
const packageJsonPath = path.join(ROOT, 'package.json');
|
const packageJsonPath = path.join(ROOT, 'package.json');
|
||||||
|
|
||||||
@ -101,7 +39,6 @@ export function loadPackageJson() {
|
|||||||
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
||||||
*/
|
*/
|
||||||
export async function which(program: string): Promise<string> {
|
export async function which(program: string): Promise<string> {
|
||||||
const whichMod = await import('which');
|
|
||||||
let programPath: string;
|
let programPath: string;
|
||||||
try {
|
try {
|
||||||
programPath = await whichMod(program);
|
programPath = await whichMod(program);
|
||||||
@ -132,7 +69,7 @@ export async function whichSpawn(
|
|||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.on('close', resolve);
|
.on('close', resolve);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err as Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -57,7 +57,10 @@ require('ts-node').register({
|
|||||||
project: path.join(rootDir, 'tsconfig.json'),
|
project: path.join(rootDir, 'tsconfig.json'),
|
||||||
transpileOnly: true,
|
transpileOnly: true,
|
||||||
});
|
});
|
||||||
require('../src/app').run(undefined, { dir: __dirname, development: true });
|
void require('../src/app').run(undefined, {
|
||||||
|
dir: __dirname,
|
||||||
|
development: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Modify package.json oclif paths from build/ -> src/, or vice versa
|
// Modify package.json oclif paths from build/ -> src/, or vice versa
|
||||||
function modifyOclifPaths(revert) {
|
function modifyOclifPaths(revert) {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
process.env.UV_THREADPOOL_SIZE = '64';
|
process.env.UV_THREADPOOL_SIZE = '64';
|
||||||
|
|
||||||
// Disable oclif registering ts-node
|
// Disable oclif registering ts-node
|
||||||
process.env.OCLIF_TS_NODE = 0;
|
process.env.OCLIF_TS_NODE = '0';
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
// Use fast-boot to cache require lookups, speeding up startup
|
// Use fast-boot to cache require lookups, speeding up startup
|
||||||
@ -18,4 +18,4 @@ async function run() {
|
|||||||
await require('../build/app').run(undefined, { dir: __dirname });
|
await require('../build/app').run(undefined, { dir: __dirname });
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
void run();
|
||||||
|
@ -13,6 +13,8 @@ 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
|
||||||
@ -326,6 +328,8 @@ 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
|
||||||
|
|
||||||
@ -333,6 +337,10 @@ 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
|
||||||
@ -568,9 +576,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 anauthenticated root ssh access and exposing network ports such as
|
allowing unauthenticated 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 partion. Development
|
to be inserted in the 'config.json' file in the image's boot partition. 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
|
||||||
@ -2866,9 +2874,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 anauthenticated root ssh access and exposing network ports such as
|
allowing unauthenticated 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 partion. Development
|
to be inserted in the 'config.json' file in the image's boot partition. 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
|
||||||
|
32
eslint.config.js
Normal file
32
eslint.config.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
...require('@balena/lint/config/eslint.config'),
|
||||||
|
...compat.config({
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.dev.json',
|
||||||
|
},
|
||||||
|
ignorePatterns: ['**/generate-completion.js', '**/bin/**/*'],
|
||||||
|
rules: {
|
||||||
|
ignoreDefinitionFiles: 0,
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
|
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||||
|
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||||
|
|
||||||
|
'no-restricted-imports': ['error', {
|
||||||
|
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
|
||||||
|
}],
|
||||||
|
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', {
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
8695
npm-shrinkwrap.json
generated
8695
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
71
package.json
71
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "balena-cli",
|
"name": "balena-cli",
|
||||||
"version": "20.0.1",
|
"version": "22.1.1",
|
||||||
"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",
|
||||||
@ -24,26 +24,6 @@
|
|||||||
"bin": {
|
"bin": {
|
||||||
"balena": "./bin/run.js"
|
"balena": "./bin/run.js"
|
||||||
},
|
},
|
||||||
"pkg": {
|
|
||||||
"scripts": [
|
|
||||||
"build/**/*.js",
|
|
||||||
"node_modules/balena-sdk/es2018/index.js",
|
|
||||||
"node_modules/pinejs-client-request/node_modules/pinejs-client-core/es2018/index.js",
|
|
||||||
"node_modules/@balena/compose/dist/parse/schemas/*.json"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
"build/auth/pages/*.ejs",
|
|
||||||
"node_modules/balena-sdk/node_modules/balena-pine/**/*",
|
|
||||||
"node_modules/balena-pine/**/*",
|
|
||||||
"node_modules/pinejs-client-core/**/*",
|
|
||||||
"node_modules/open/xdg-open",
|
|
||||||
"node_modules/windosu/*.bat",
|
|
||||||
"node_modules/windosu/*.cmd",
|
|
||||||
"node_modules/axios/**/*",
|
|
||||||
"npm-shrinkwrap.json",
|
|
||||||
"oclif.manifest.json"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "node patches/apply-patches.js",
|
"postinstall": "node patches/apply-patches.js",
|
||||||
"prebuild": "rimraf build/ build-bin/",
|
"prebuild": "rimraf build/ build-bin/",
|
||||||
@ -58,6 +38,7 @@
|
|||||||
"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",
|
||||||
@ -91,7 +72,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"
|
"node": ">=20.6.0 <23"
|
||||||
},
|
},
|
||||||
"oclif": {
|
"oclif": {
|
||||||
"bin": "balena",
|
"bin": "balena",
|
||||||
@ -111,16 +92,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@balena/lint": "^8.0.0",
|
"@balena/lint": "^9.1.3",
|
||||||
"@electron/notarize": "^2.0.0",
|
"@electron/notarize": "^2.0.0",
|
||||||
"@types/archiver": "^6.0.2",
|
|
||||||
"@types/bluebird": "^3.5.36",
|
"@types/bluebird": "^3.5.36",
|
||||||
"@types/body-parser": "^1.19.2",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/chai": "^4.3.0",
|
"@types/chai": "^4.3.0",
|
||||||
"@types/chai-as-promised": "^7.1.4",
|
"@types/chai-as-promised": "^7.1.4",
|
||||||
"@types/cli-truncate": "^2.0.0",
|
"@types/cli-truncate": "^2.0.0",
|
||||||
"@types/common-tags": "^1.8.1",
|
"@types/common-tags": "^1.8.1",
|
||||||
"@types/diff": "^5.0.3",
|
|
||||||
"@types/dockerode": "3.3.23",
|
"@types/dockerode": "3.3.23",
|
||||||
"@types/ejs": "^3.1.0",
|
"@types/ejs": "^3.1.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
@ -145,7 +124,6 @@
|
|||||||
"@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",
|
||||||
@ -159,16 +137,12 @@
|
|||||||
"@types/update-notifier": "^4.1.1",
|
"@types/update-notifier": "^4.1.1",
|
||||||
"@types/which": "^2.0.1",
|
"@types/which": "^2.0.1",
|
||||||
"@types/window-size": "^1.1.1",
|
"@types/window-size": "^1.1.1",
|
||||||
"@yao-pkg/pkg": "^5.11.1",
|
|
||||||
"archiver": "^7.0.1",
|
|
||||||
"catch-uncommitted": "^2.0.0",
|
"catch-uncommitted": "^2.0.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"deep-object-diff": "^1.1.0",
|
"deep-object-diff": "^1.1.0",
|
||||||
"diff": "^5.0.0",
|
|
||||||
"ent": "^2.2.0",
|
"ent": "^2.2.0",
|
||||||
"filehound": "^1.17.5",
|
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
"husky": "^9.1.5",
|
"husky": "^9.1.5",
|
||||||
@ -179,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": "^13.2.1",
|
"nock": "^14.0.4",
|
||||||
"oclif": "^4.14.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.6.2"
|
"typescript": "^5.8.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@balena/compose": "^5.0.0",
|
"@balena/compose": "^7.0.9",
|
||||||
"@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.0.31",
|
"@oclif/core": "^4.1.0",
|
||||||
"@sentry/node": "^6.16.1",
|
"@sentry/node": "^9.0.0",
|
||||||
"balena-config-json": "^4.2.0",
|
"balena-config-json": "^4.2.7",
|
||||||
"balena-device-init": "^7.0.1",
|
"balena-device-init": "^8.1.11",
|
||||||
"balena-errors": "^4.7.3",
|
"balena-errors": "^4.7.3",
|
||||||
"balena-image-fs": "^7.0.6",
|
"balena-image-fs": "^7.5.2",
|
||||||
"balena-preload": "^16.0.0",
|
"balena-preload": "^18.0.4",
|
||||||
"balena-sdk": "^20.3.0",
|
"balena-sdk": "^21.3.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",
|
||||||
@ -211,12 +185,13 @@
|
|||||||
"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.3",
|
"docker-modem": "^5.0.6",
|
||||||
"docker-progress": "^5.1.3",
|
"docker-progress": "^5.1.3",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.5",
|
||||||
"ejs": "^3.1.6",
|
"ejs": "^3.1.6",
|
||||||
"etcher-sdk": "9.1.0",
|
"etcher-sdk": "^10.0.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",
|
||||||
@ -231,6 +206,7 @@
|
|||||||
"is-root": "^2.1.0",
|
"is-root": "^2.1.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"JSONStream": "^1.0.3",
|
"JSONStream": "^1.0.3",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"livepush": "^3.5.1",
|
"livepush": "^3.5.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^2.4.6",
|
"mime": "^2.4.6",
|
||||||
@ -243,9 +219,8 @@
|
|||||||
"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",
|
||||||
"request": "^2.88.2",
|
"resin-cli-form": "^4.0.0",
|
||||||
"resin-cli-form": "^3.0.0",
|
"resin-cli-visuals": "^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",
|
||||||
@ -273,6 +248,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"versionist": {
|
"versionist": {
|
||||||
"publishedAt": "2024-10-29T11:37:11.260Z"
|
"publishedAt": "2025-06-19T09:32:53.877Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
|
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
|
||||||
index ef7f90e..8264b7c 100644
|
index bfe9205..482519e 100644
|
||||||
--- a/node_modules/oclif/lib/commands/pack/win.js
|
--- a/node_modules/oclif/lib/commands/pack/win.js
|
||||||
+++ b/node_modules/oclif/lib/commands/pack/win.js
|
+++ b/node_modules/oclif/lib/commands/pack/win.js
|
||||||
@@ -76,6 +76,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
|
@@ -86,6 +86,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
|
||||||
${customization}
|
${customization}
|
||||||
|
|
||||||
Section "${config.name} CLI \${VERSION}"
|
Section "${config.name} CLI \${VERSION}"
|
||||||
@ -16,20 +16,18 @@ index ef7f90e..8264b7c 100644
|
|||||||
File /r bin
|
File /r bin
|
||||||
File /r client
|
File /r client
|
||||||
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
|
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
|
||||||
index 14d5a6e..7b42a6f 100644
|
index f0c8d95..a72400e 100644
|
||||||
--- a/node_modules/oclif/lib/tarballs/build.js
|
--- a/node_modules/oclif/lib/tarballs/build.js
|
||||||
+++ b/node_modules/oclif/lib/tarballs/build.js
|
+++ b/node_modules/oclif/lib/tarballs/build.js
|
||||||
@@ -200,6 +200,13 @@ const extractCLI = async (tarball, c) => {
|
@@ -218,6 +218,11 @@ const extractCLI = async (tarball, c) => {
|
||||||
(0, promises_1.rm)(path.join(workspace, path.basename(tarball)), { recursive: true }),
|
(0, promises_1.rm)(path.join(workspace, path.basename(tarball)), { recursive: true }),
|
||||||
(0, fs_extra_1.remove)(path.join(workspace, 'bin', 'run.cmd')),
|
(0, fs_extra_1.remove)(path.join(workspace, 'bin', 'run.cmd')),
|
||||||
]);
|
]);
|
||||||
+
|
|
||||||
+ // The oclif installers are a production installation, while the source
|
+ // The oclif installers are a production installation, while the source
|
||||||
+ // `bin` folder may contain a `.fast-boot.json` file of a dev installation.
|
+ // `bin` folder may contain a `.fast-boot.json` file of a dev installation.
|
||||||
+ // This has previously led to issues preventing the CLI from starting, so
|
+ // This has previously led to issues preventing the CLI from starting, so
|
||||||
+ // delete `.fast-boot.json` (if any) from the destination folder.
|
+ // delete `.fast-boot.json` (if any) from the destination folder.
|
||||||
+ await (0, fs_extra_1.remove)(path.join(workspace, 'bin', '.fast-boot.json'));
|
+ await (0, fs_extra_1.remove)(path.join(workspace, 'bin', '.fast-boot.json'));
|
||||||
+
|
|
||||||
};
|
};
|
||||||
const buildTarget = async (target, c, options) => {
|
const buildTarget = async (target, c, options) => {
|
||||||
const workspace = c.workspace(target);
|
if (target.platform === 'win32' && target.arch === 'arm64' && (0, semver_1.lt)(c.nodeVersion, '20.0.0')) {
|
@ -1,16 +0,0 @@
|
|||||||
diff --git a/node_modules/open/index.js b/node_modules/open/index.js
|
|
||||||
index 13147d0..ff161dd 100644
|
|
||||||
--- a/node_modules/open/index.js
|
|
||||||
+++ b/node_modules/open/index.js
|
|
||||||
@@ -10,7 +10,10 @@ const pAccess = promisify(fs.access);
|
|
||||||
const pReadFile = promisify(fs.readFile);
|
|
||||||
|
|
||||||
// Path to included `xdg-open`.
|
|
||||||
-const localXdgOpenPath = path.join(__dirname, 'xdg-open');
|
|
||||||
+const localXdgOpenPath = process.pkg
|
|
||||||
+ ? path.join(path.dirname(process.execPath), 'xdg-open')
|
|
||||||
+ : path.join(__dirname, 'xdg-open');
|
|
||||||
+
|
|
||||||
|
|
||||||
/**
|
|
||||||
Get the mount point for fixed drives in WSL.
|
|
@ -1,14 +0,0 @@
|
|||||||
diff --git a/node_modules/node-gyp-build/node-gyp-build.js b/node_modules/node-gyp-build/node-gyp-build.js
|
|
||||||
index 61b398e..3cc3be8 100644
|
|
||||||
--- a/node_modules/node-gyp-build/node-gyp-build.js
|
|
||||||
+++ b/node_modules/node-gyp-build/node-gyp-build.js
|
|
||||||
@@ -30,6 +30,9 @@ load.resolve = load.path = function (dir) {
|
|
||||||
if (process.env[name + '_PREBUILD']) dir = process.env[name + '_PREBUILD']
|
|
||||||
} catch (err) {}
|
|
||||||
|
|
||||||
+ // pkg fix: native node modules are located externally to the pkg executable
|
|
||||||
+ dir = dir.replace(/^\/snapshot\/.+?\/node_modules\//, path.dirname(process.execPath) + path.sep)
|
|
||||||
+
|
|
||||||
if (!prebuildsOnly) {
|
|
||||||
var release = getFirst(path.join(dir, 'build/Release'), matchBuild)
|
|
||||||
if (release) return release
|
|
@ -1,38 +0,0 @@
|
|||||||
diff --git a/node_modules/windosu/lib/pipe.js b/node_modules/windosu/lib/pipe.js
|
|
||||||
index dc81fa5..a381cc7 100644
|
|
||||||
--- a/node_modules/windosu/lib/pipe.js
|
|
||||||
+++ b/node_modules/windosu/lib/pipe.js
|
|
||||||
@@ -42,7 +42,8 @@ function pipe(path, options) {
|
|
||||||
return d.promise;
|
|
||||||
}
|
|
||||||
module.exports = pipe;
|
|
||||||
-if (module === require.main) {
|
|
||||||
+
|
|
||||||
+function main() {
|
|
||||||
if (!process.argv[4]) {
|
|
||||||
console.error('Incorrect arguments!');
|
|
||||||
process.exit(-1);
|
|
||||||
@@ -52,3 +53,8 @@ if (module === require.main) {
|
|
||||||
serve: process.argv[3] == 'server'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+module.exports.main = main;
|
|
||||||
+
|
|
||||||
+if (module === require.main) {
|
|
||||||
+ main();
|
|
||||||
+}
|
|
||||||
diff --git a/node_modules/windosu/lib/windosu.js b/node_modules/windosu/lib/windosu.js
|
|
||||||
index 6502812..dd0391a 100644
|
|
||||||
--- a/node_modules/windosu/lib/windosu.js
|
|
||||||
+++ b/node_modules/windosu/lib/windosu.js
|
|
||||||
@@ -16,7 +16,9 @@ module.exports.exec = function (command, options, callback) {
|
|
||||||
temp: temp,
|
|
||||||
command: command,
|
|
||||||
cliWidth: cliWidth(),
|
|
||||||
- pipe: '"' + process.execPath + '" "' + path.join(__dirname, 'pipe.js') + '"',
|
|
||||||
+ pipe: process.pkg
|
|
||||||
+ ? '"' + process.execPath + '" pkgExec "' + path.join(__dirname, 'pipe.js') + '::main"'
|
|
||||||
+ : '"' + process.execPath + '" "' + path.join(__dirname, 'pipe.js') + '"',
|
|
||||||
input: inputName = id + '-in',
|
|
||||||
output: outputName = id + '-out',
|
|
||||||
stderr_redir: process.stdout.isTTY ? '2>&1' : '2> %ERROR%'
|
|
4
repo.yml
4
repo.yml
@ -6,6 +6,10 @@ 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'
|
||||||
|
url: 'https://github.com/balena-io-modules/balena-device-init'
|
||||||
- repo: 'balena-image-manager'
|
- repo: 'balena-image-manager'
|
||||||
url: 'https://github.com/balena-io-modules/balena-image-manager'
|
url: 'https://github.com/balena-io-modules/balena-image-manager'
|
||||||
- repo: 'balena-preload'
|
- repo: 'balena-preload'
|
||||||
|
26
src/app.ts
26
src/app.ts
@ -34,18 +34,14 @@ 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.configureScope((scope) => {
|
Sentry.getCurrentScope().setExtras({
|
||||||
scope.setExtras({
|
is_pkg: !!(process as any).pkg,
|
||||||
is_pkg: !!(process as any).pkg,
|
node_version: process.version,
|
||||||
node_version: process.version,
|
platform: process.platform,
|
||||||
platform: process.platform,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
return Sentry.getCurrentHub();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function checkNodeVersion() {
|
async function checkNodeVersion() {
|
||||||
@ -101,11 +97,9 @@ async function init() {
|
|||||||
|
|
||||||
/** Execute the oclif parser and the CLI command. */
|
/** Execute the oclif parser and the CLI command. */
|
||||||
async function oclifRun(command: string[], options: AppOptions) {
|
async function oclifRun(command: string[], options: AppOptions) {
|
||||||
let deprecationPromise: Promise<void>;
|
let deprecationPromise: Promise<void> | undefined;
|
||||||
// check and enforce the CLI's deprecation policy
|
// check and enforce the CLI's deprecation policy
|
||||||
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
if (!(unsupportedFlag || process.env.BALENARC_UNSUPPORTED)) {
|
||||||
deprecationPromise = Promise.resolve();
|
|
||||||
} else {
|
|
||||||
const { DeprecationChecker } = await import('./deprecation');
|
const { DeprecationChecker } = await import('./deprecation');
|
||||||
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||||
// warnAndAbortIfDeprecated uses previously cached data only
|
// warnAndAbortIfDeprecated uses previously cached data only
|
||||||
@ -161,18 +155,12 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
/** CLI entrypoint. Called by the `bin/run.js` and `bin/dev.js` scripts. */
|
/** CLI entrypoint. Called by the `bin/run.js` and `bin/dev.js` scripts. */
|
||||||
export async function run(cliArgs = process.argv, options: AppOptions) {
|
export async function run(cliArgs = process.argv, options: AppOptions) {
|
||||||
try {
|
try {
|
||||||
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
const { setOfflineModeEnvVars, normalizeEnvVars } = await import(
|
||||||
'./utils/bootstrap'
|
'./utils/bootstrap'
|
||||||
);
|
);
|
||||||
setOfflineModeEnvVars();
|
setOfflineModeEnvVars();
|
||||||
normalizeEnvVars();
|
normalizeEnvVars();
|
||||||
|
|
||||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
|
||||||
// for use of the standalone zip package. See pkgExec function.
|
|
||||||
if (cliArgs.length > 3 && cliArgs[2] === 'pkgExec') {
|
|
||||||
return pkgExec(cliArgs[3], cliArgs.slice(4));
|
|
||||||
}
|
|
||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
// Look for commands that have been removed and if so, exit with a notice
|
// Look for commands that have been removed and if so, exit with a notice
|
||||||
|
@ -17,7 +17,28 @@
|
|||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { Args, Command } from '@oclif/core';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||||
|
import {
|
||||||
|
formatDuration,
|
||||||
|
intervalToDuration,
|
||||||
|
isValid,
|
||||||
|
parseISO,
|
||||||
|
} from 'date-fns';
|
||||||
|
|
||||||
|
// In days
|
||||||
|
const durations = [1, 7, 30, 90];
|
||||||
|
|
||||||
|
async function isLoggedInWithJwt() {
|
||||||
|
const balena = getBalenaSdk();
|
||||||
|
try {
|
||||||
|
const token = await balena.auth.getToken();
|
||||||
|
const { default: jwtDecode } = await import('jwt-decode');
|
||||||
|
jwtDecode(token);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class GenerateCmd extends Command {
|
export default class GenerateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -29,13 +50,21 @@ 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 = ['$ balena api-key generate "Jenkins Key"'];
|
public static examples = [
|
||||||
|
'$ balena api-key generate "Jenkins Key"',
|
||||||
|
'$ balena api-key generate "Jenkins Key" 2025-10-30',
|
||||||
|
'$ balena api-key generate "Jenkins Key" never',
|
||||||
|
];
|
||||||
|
|
||||||
public static args = {
|
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;
|
||||||
@ -43,11 +72,70 @@ 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 {
|
||||||
key = await getBalenaSdk().models.apiKey.create(params.name);
|
if (!expiryDateResponse) {
|
||||||
|
expiryDateResponse = await getCliForm().ask({
|
||||||
|
message: 'Please pick an expiry date for the API key',
|
||||||
|
type: 'list',
|
||||||
|
choices: [...durations, 'custom', 'never'].map((duration) => ({
|
||||||
|
name:
|
||||||
|
duration === 'never'
|
||||||
|
? 'No expiration'
|
||||||
|
: typeof duration === 'number'
|
||||||
|
? formatDuration(
|
||||||
|
intervalToDuration({
|
||||||
|
start: 0,
|
||||||
|
end: duration * 24 * 60 * 60 * 1000,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: 'Custom expiration',
|
||||||
|
value: duration,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let expiryDate: Date | null;
|
||||||
|
if (expiryDateResponse === 'never') {
|
||||||
|
expiryDate = null;
|
||||||
|
} else if (expiryDateResponse === 'custom') {
|
||||||
|
do {
|
||||||
|
expiryDate = parseISO(
|
||||||
|
await getCliForm().ask({
|
||||||
|
message:
|
||||||
|
'Please enter an expiry date for the API key as an ISO date string',
|
||||||
|
type: 'input',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (!isValid(expiryDate)) {
|
||||||
|
console.error('Invalid date format');
|
||||||
|
}
|
||||||
|
} while (!isValid(expiryDate));
|
||||||
|
} else if (typeof expiryDateResponse === 'string') {
|
||||||
|
expiryDate = parseISO(expiryDateResponse);
|
||||||
|
if (!isValid(expiryDate)) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid date format, please use a valid ISO date string',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expiryDate = new Date(
|
||||||
|
Date.now() + expiryDateResponse * 24 * 60 * 60 * 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
key = await getBalenaSdk().models.apiKey.create({
|
||||||
|
name: params.name,
|
||||||
|
expiryDate: expiryDate === null ? null : expiryDate.toISOString(),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name === 'BalenaNotLoggedIn') {
|
if (e.name === 'BalenaNotLoggedIn') {
|
||||||
|
if (await isLoggedInWithJwt()) {
|
||||||
|
throw new ExpectedError(stripIndent`
|
||||||
|
This command requires you to have been recently authenticated.
|
||||||
|
Please login again with 'balena login'.
|
||||||
|
In case you are using the Web authorization method, you need to logout and re-login to the dashboard first.
|
||||||
|
`);
|
||||||
|
}
|
||||||
throw new ExpectedError(stripIndent`
|
throw new ExpectedError(stripIndent`
|
||||||
This command cannot be run when logged in with an API key.
|
This command cannot be run when logged in with an API key.
|
||||||
Please login again with 'balena login' and select an alternative method.
|
Please login again with 'balena login' and select an alternative method.
|
||||||
|
@ -50,9 +50,9 @@ export default class RevokeCmd extends Command {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
apiKeyIds.map(
|
apiKeyIds.map(async (id) => {
|
||||||
async (id) => await getBalenaSdk().models.apiKey.revoke(Number(id)),
|
await getBalenaSdk().models.apiKey.revoke(Number(id));
|
||||||
),
|
}),
|
||||||
);
|
);
|
||||||
console.log('Successfully revoked the given API keys');
|
console.log('Successfully revoked the given API keys');
|
||||||
}
|
}
|
||||||
|
@ -36,14 +36,16 @@ import { buildProject, composeCliFlags } from '../../utils/compose_ts';
|
|||||||
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||||
import { dockerCliFlags } from '../../utils/docker';
|
import { dockerCliFlags } from '../../utils/docker';
|
||||||
|
|
||||||
// TODO: For this special one we can't use Interfaces.InferredFlags/InferredArgs
|
type ComposeGenerateOptsParam = Parameters<typeof compose.generateOpts>[0];
|
||||||
// because of the 'registry-secrets' type which is defined in the actual code
|
|
||||||
// as a path (string | undefined) but then the cli turns it into an object
|
interface PrepareBuildOpts
|
||||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
extends ComposeCliFlags,
|
||||||
|
DockerCliFlags,
|
||||||
|
ComposeGenerateOptsParam {
|
||||||
arch?: string;
|
arch?: string;
|
||||||
deviceType?: string;
|
deviceType?: string;
|
||||||
fleet?: string;
|
fleet?: string;
|
||||||
source?: string; // Not part of command profile - source param copied here.
|
source?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class BuildCmd extends Command {
|
export default class BuildCmd extends Command {
|
||||||
@ -113,29 +115,31 @@ ${dockerignoreHelp}
|
|||||||
const logger = Logger.getLogger();
|
const logger = Logger.getLogger();
|
||||||
logger.logDebug('Parsing input...');
|
logger.logDebug('Parsing input...');
|
||||||
|
|
||||||
// `build` accepts `source` as a parameter, but compose expects it as an option
|
const prepareBuildOpts = {
|
||||||
options.source = params.source;
|
...options,
|
||||||
delete params.source;
|
source: params.source,
|
||||||
|
};
|
||||||
|
|
||||||
await this.resolveArchFromDeviceType(sdk, options);
|
await this.resolveArchFromDeviceType(sdk, prepareBuildOpts);
|
||||||
|
|
||||||
await this.validateOptions(options, sdk);
|
await this.validateOptions(prepareBuildOpts, sdk);
|
||||||
|
|
||||||
// Build args are under consideration for removal - warn user
|
// Build args are under consideration for removal - warn user
|
||||||
if (options.buildArg) {
|
if (prepareBuildOpts.buildArg) {
|
||||||
console.log(buildArgDeprecation);
|
console.log(buildArgDeprecation);
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = await this.getAppAndResolveArch(options);
|
const app = await this.getAppAndResolveArch(prepareBuildOpts);
|
||||||
|
|
||||||
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
|
const { docker, buildOpts, composeOpts } =
|
||||||
|
await this.prepareBuild(prepareBuildOpts);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.buildProject(docker, logger, composeOpts, {
|
await this.buildProject(docker, logger, composeOpts, {
|
||||||
appType: app?.application_type?.[0],
|
appType: app?.application_type?.[0],
|
||||||
arch: options.arch!,
|
arch: prepareBuildOpts.arch!,
|
||||||
deviceType: options.deviceType!,
|
deviceType: prepareBuildOpts.deviceType!,
|
||||||
buildEmulated: options.emulated,
|
buildEmulated: prepareBuildOpts.emulated,
|
||||||
buildOpts,
|
buildOpts,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -147,7 +151,7 @@ ${dockerignoreHelp}
|
|||||||
logger.logSuccess('Build succeeded!');
|
logger.logSuccess('Build succeeded!');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
protected async validateOptions(opts: PrepareBuildOpts, sdk: BalenaSDK) {
|
||||||
// Validate option combinations
|
// Validate option combinations
|
||||||
if (
|
if (
|
||||||
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
||||||
@ -175,7 +179,10 @@ ${dockerignoreHelp}
|
|||||||
opts['registry-secrets'] = registrySecrets;
|
opts['registry-secrets'] = registrySecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async resolveArchFromDeviceType(sdk: BalenaSDK, opts: FlagsDef) {
|
protected async resolveArchFromDeviceType(
|
||||||
|
sdk: BalenaSDK,
|
||||||
|
opts: PrepareBuildOpts,
|
||||||
|
) {
|
||||||
if (opts.deviceType != null && opts.arch == null) {
|
if (opts.deviceType != null && opts.arch == null) {
|
||||||
try {
|
try {
|
||||||
const deviceTypeOpts = {
|
const deviceTypeOpts = {
|
||||||
@ -208,7 +215,7 @@ ${dockerignoreHelp}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getAppAndResolveArch(opts: FlagsDef) {
|
protected async getAppAndResolveArch(opts: PrepareBuildOpts) {
|
||||||
if (opts.fleet) {
|
if (opts.fleet) {
|
||||||
const { getAppWithArch } = await import('../../utils/helpers');
|
const { getAppWithArch } = await import('../../utils/helpers');
|
||||||
const app = await getAppWithArch(opts.fleet);
|
const app = await getAppWithArch(opts.fleet);
|
||||||
@ -218,7 +225,7 @@ ${dockerignoreHelp}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async prepareBuild(options: FlagsDef) {
|
protected async prepareBuild(options: PrepareBuildOpts) {
|
||||||
const { getDocker, generateBuildOpts } = await import('../../utils/docker');
|
const { getDocker, generateBuildOpts } = await import('../../utils/docker');
|
||||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||||
getDocker(options),
|
getDocker(options),
|
||||||
|
@ -64,7 +64,12 @@ export default class ConfigInjectCmd extends Command {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
await config.write(drive, '', configJSON);
|
await config.write(
|
||||||
|
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,14 +64,19 @@ 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(drive, '', configJSON);
|
await config.write(
|
||||||
|
drive,
|
||||||
|
// Will be removed in the next major of balena-config-json
|
||||||
|
undefined,
|
||||||
|
configJSON,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.info('Done');
|
console.info('Done');
|
||||||
|
@ -368,6 +368,7 @@ ${dockerignoreHelp}
|
|||||||
!opts.shouldUploadLogs,
|
!opts.shouldUploadLogs,
|
||||||
composeOpts.projectPath,
|
composeOpts.projectPath,
|
||||||
opts.createAsDraft,
|
opts.createAsDraft,
|
||||||
|
project.descriptors,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ export default class DeviceDetectCmd extends Command {
|
|||||||
try {
|
try {
|
||||||
await docker.ping();
|
await docker.ping();
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -77,45 +77,59 @@ export default class DeviceCmd extends Command {
|
|||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const device = (await balena.models.device.get(
|
let device: ExtendedDevice;
|
||||||
params.uuid,
|
if (options.json) {
|
||||||
options.json
|
const [deviceBase, deviceComputed] = await Promise.all([
|
||||||
? {
|
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');
|
||||||
|
@ -155,7 +155,7 @@ export default class DeviceInitCmd extends Command {
|
|||||||
try {
|
try {
|
||||||
logger.logDebug(`Process failed, removing device ${device.uuid}`);
|
logger.logDebug(`Process failed, removing device ${device.uuid}`);
|
||||||
await balena.models.device.remove(device.uuid);
|
await balena.models.device.remove(device.uuid);
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Ignore removal failures, and throw original error
|
// Ignore removal failures, and throw original error
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -135,7 +135,7 @@ export default class DeviceLogsCmd extends Command {
|
|||||||
logger.logDebug('Checking we can access device');
|
logger.logDebug('Checking we can access device');
|
||||||
try {
|
try {
|
||||||
await deviceApi.ping();
|
await deviceApi.ping();
|
||||||
} catch (e) {
|
} catch {
|
||||||
const { ExpectedError } = await import('../../errors');
|
const { ExpectedError } = await import('../../errors');
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`Cannot access device at address ${params.device}. Device may not be in local mode.`,
|
`Cannot access device at address ${params.device}. Device may not be in local mode.`,
|
||||||
|
@ -20,6 +20,7 @@ import * as cf from '../../utils/common-flags';
|
|||||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||||
import type { Device } from 'balena-sdk';
|
import type { Device } from 'balena-sdk';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
import { getExpandedProp } from '../../utils/pine';
|
||||||
|
|
||||||
export default class DeviceOsUpdateCmd extends Command {
|
export default class DeviceOsUpdateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -126,20 +127,46 @@ export default class DeviceOsUpdateCmd extends Command {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const choices = await Promise.all(
|
||||||
|
hupVersionInfo.versions.map(async (version) => {
|
||||||
|
const takeoverRequired =
|
||||||
|
(await sdk.models.os.getOsUpdateType(
|
||||||
|
getExpandedProp(is_of__device_type, 'slug')!,
|
||||||
|
currentOsVersion,
|
||||||
|
version,
|
||||||
|
)) === 'takeover';
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${version}${hupVersionInfo.recommended === version ? ' (recommended)' : ''}${takeoverRequired ? ' ADVANCED UPDATE: Requires disk re-partitioning with no rollback option' : ''}`,
|
||||||
|
value: version,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
targetOsVersion = await getCliForm().ask({
|
targetOsVersion = await getCliForm().ask({
|
||||||
message: 'Target OS version',
|
message: 'Target OS version',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
choices: hupVersionInfo.versions.map((version) => ({
|
choices,
|
||||||
name:
|
|
||||||
hupVersionInfo.recommended === version
|
|
||||||
? `${version} (recommended)`
|
|
||||||
: version,
|
|
||||||
value: version,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const takeoverRequired =
|
||||||
|
(await sdk.models.os.getOsUpdateType(
|
||||||
|
getExpandedProp(is_of__device_type, 'slug')!,
|
||||||
|
currentOsVersion,
|
||||||
|
targetOsVersion,
|
||||||
|
)) === 'takeover';
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
|
// Warn the user if the update requires a takeover
|
||||||
|
if (takeoverRequired) {
|
||||||
|
await patterns.confirm(
|
||||||
|
options.yes || false,
|
||||||
|
stripIndent`Before you proceed, note that this update process is different from a regular HostOS Update:
|
||||||
|
DATA LOSS: This update requires disk re-partitioning, which will erase all data stored on the device.
|
||||||
|
NO ROLLBACK: Unlike our HostOS update mechanism, this process does not allow reverting to a previous version in case of failure.
|
||||||
|
Make sure to back up all important data before continuing. For more details, check our documentation: https://docs.balena.io/reference/OS/updates/update-process/
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
// Confirm and start update
|
// Confirm and start update
|
||||||
await patterns.confirm(
|
await patterns.confirm(
|
||||||
options.yes || false,
|
options.yes || false,
|
||||||
|
@ -76,6 +76,6 @@ export default class DeviceRegisterCmd extends Command {
|
|||||||
options.deviceType,
|
options.deviceType,
|
||||||
);
|
);
|
||||||
|
|
||||||
return result && result.uuid;
|
return result.uuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ export default class DeviceSSHCmd extends Command {
|
|||||||
SSH server port number (default 22222) if the target is an IP address or .local
|
SSH server port number (default 22222) if the target is an IP address or .local
|
||||||
hostname. Otherwise, port number for the balenaCloud gateway (default 22).`,
|
hostname. Otherwise, port number for the balenaCloud gateway (default 22).`,
|
||||||
char: 'p',
|
char: 'p',
|
||||||
parse: async (p) => parseAsInteger(p, 'port'),
|
parse: (p) => parseAsInteger(p, 'port'),
|
||||||
}),
|
}),
|
||||||
tty: Flags.boolean({
|
tty: Flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
@ -110,13 +110,14 @@ export default class DeviceSSHCmd extends Command {
|
|||||||
// Local connection
|
// Local connection
|
||||||
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
|
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
|
||||||
const { performLocalDeviceSSH } = await import('../../utils/device/ssh');
|
const { performLocalDeviceSSH } = await import('../../utils/device/ssh');
|
||||||
return await performLocalDeviceSSH({
|
await performLocalDeviceSSH({
|
||||||
hostname: params.fleetOrDevice,
|
hostname: params.fleetOrDevice,
|
||||||
port: options.port || 'local',
|
port: options.port || 'local',
|
||||||
forceTTY: options.tty,
|
forceTTY: options.tty,
|
||||||
verbose: options.verbose,
|
verbose: options.verbose,
|
||||||
service: params.service,
|
service: params.service,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remote connection
|
// Remote connection
|
||||||
@ -132,7 +133,7 @@ export default class DeviceSSHCmd extends Command {
|
|||||||
const useProxy = !!proxyConfig && !options.noproxy;
|
const useProxy = !!proxyConfig && !options.noproxy;
|
||||||
|
|
||||||
// this will be a tunnelled SSH connection...
|
// this will be a tunnelled SSH connection...
|
||||||
await checkNotUsingOfflineMode();
|
checkNotUsingOfflineMode();
|
||||||
await checkLoggedIn();
|
await checkLoggedIn();
|
||||||
const deviceUuid = await getOnlineTargetDeviceUuid(
|
const deviceUuid = await getOnlineTargetDeviceUuid(
|
||||||
sdk,
|
sdk,
|
||||||
|
2
src/commands/env/rename.ts
vendored
2
src/commands/env/rename.ts
vendored
@ -41,7 +41,7 @@ export default class EnvRenameCmd extends Command {
|
|||||||
id: Args.integer({
|
id: Args.integer({
|
||||||
required: true,
|
required: true,
|
||||||
description: "variable's numeric database ID",
|
description: "variable's numeric database ID",
|
||||||
parse: async (input) => parseAsInteger(input, 'id'),
|
parse: (input) => parseAsInteger(input, 'id'),
|
||||||
}),
|
}),
|
||||||
value: Args.string({
|
value: Args.string({
|
||||||
required: true,
|
required: true,
|
||||||
|
2
src/commands/env/rm.ts
vendored
2
src/commands/env/rm.ts
vendored
@ -46,7 +46,7 @@ export default class EnvRmCmd extends Command {
|
|||||||
id: Args.integer({
|
id: Args.integer({
|
||||||
required: true,
|
required: true,
|
||||||
description: "variable's numeric database ID",
|
description: "variable's numeric database ID",
|
||||||
parse: async (input) => parseAsInteger(input, 'id'),
|
parse: (input) => parseAsInteger(input, 'id'),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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 {
|
||||||
@ -237,7 +236,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 promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
|
return await _fs.promises.readdir(this.CONNECTIONS_FOLDER);
|
||||||
});
|
});
|
||||||
|
|
||||||
let connectionFileName;
|
let connectionFileName;
|
||||||
@ -246,13 +245,11 @@ 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 readFileAsync = promisify(_fs.readFile);
|
const contents = await _fs.promises.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' },
|
||||||
);
|
);
|
||||||
return await writeFileAsync(
|
await _fs.promises.writeFile(
|
||||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||||
contents,
|
contents,
|
||||||
);
|
);
|
||||||
@ -269,13 +266,13 @@ 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) => {
|
||||||
return await promisify(_fs.writeFile)(
|
await _fs.promises.writeFile(
|
||||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||||
this.CONNECTION_FILE,
|
this.CONNECTION_FILE,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await this.getConfigurationSchema(bootPartition, connectionFileName);
|
return this.getConfigurationSchema(bootPartition, connectionFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeHostname(schema: any) {
|
async removeHostname(schema: any) {
|
||||||
|
@ -132,7 +132,7 @@ export default class LoginCmd extends Command {
|
|||||||
// We can safely assume this won't be undefined as doLogin will throw if this call fails
|
// We can safely assume this won't be undefined as doLogin will throw if this call fails
|
||||||
// We also don't need to worry too much about the amount of calls to whoami
|
// We also don't need to worry too much about the amount of calls to whoami
|
||||||
// as these are cached by the SDK
|
// as these are cached by the SDK
|
||||||
const whoamiResult = (await balena.auth.whoami()) as WhoamiResult;
|
const whoamiResult = (await balena.auth.whoami())!;
|
||||||
|
|
||||||
if (whoamiResult.actorType !== 'user' && !options.hideExperimentalWarning) {
|
if (whoamiResult.actorType !== 'user' && !options.hideExperimentalWarning) {
|
||||||
console.info(stripIndent`
|
console.info(stripIndent`
|
||||||
@ -168,7 +168,7 @@ ${messages.reachingOut}`);
|
|||||||
|
|
||||||
async doLogin(
|
async doLogin(
|
||||||
loginOptions: FlagsDef,
|
loginOptions: FlagsDef,
|
||||||
balenaUrl: string = 'balena-cloud.com',
|
balenaUrl = 'balena-cloud.com',
|
||||||
token?: string,
|
token?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Token
|
// Token
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
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';
|
||||||
@ -292,7 +291,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) => {
|
||||||
return await promisify(_fs.writeFile)(
|
await _fs.promises.writeFile(
|
||||||
path.join(CONNECTIONS_FOLDER, name),
|
path.join(CONNECTIONS_FOLDER, name),
|
||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
|
@ -37,6 +37,7 @@ 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`
|
||||||
@ -109,7 +110,7 @@ https://github.com/balena-io-examples/staged-releases\
|
|||||||
'additional-space': Flags.integer({
|
'additional-space': Flags.integer({
|
||||||
description:
|
description:
|
||||||
'expand the image by this amount of bytes instead of automatically estimating the required amount',
|
'expand the image by this amount of bytes instead of automatically estimating the required amount',
|
||||||
parse: async (x) => parseAsInteger(x, 'additional-space'),
|
parse: (x) => parseAsInteger(x, 'additional-space'),
|
||||||
}),
|
}),
|
||||||
'add-certificate': Flags.string({
|
'add-certificate': Flags.string({
|
||||||
description: `\
|
description: `\
|
||||||
@ -126,7 +127,7 @@ Can be repeated to add multiple certificates.\
|
|||||||
dockerPort: Flags.integer({
|
dockerPort: Flags.integer({
|
||||||
description:
|
description:
|
||||||
'Docker daemon TCP port number (hint: 2375 for balena devices)',
|
'Docker daemon TCP port number (hint: 2375 for balena devices)',
|
||||||
parse: async (p) => parseAsInteger(p, 'dockerPort'),
|
parse: (p) => parseAsInteger(p, 'dockerPort'),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -155,12 +156,48 @@ Can be repeated to add multiple certificates.\
|
|||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`The provided image path does not exist: ${params.image}`,
|
`The provided image path does not exist: ${params.image}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
@ -192,11 +229,11 @@ Can be repeated to add multiple certificates.\
|
|||||||
event.name,
|
event.name,
|
||||||
));
|
));
|
||||||
if (event.action === 'start') {
|
if (event.action === 'start') {
|
||||||
return spinner.start();
|
spinner.start();
|
||||||
} else {
|
return;
|
||||||
console.log();
|
|
||||||
return spinner.stop();
|
|
||||||
}
|
}
|
||||||
|
console.log();
|
||||||
|
spinner.stop();
|
||||||
};
|
};
|
||||||
|
|
||||||
const commit = this.isCurrentCommit(options.commit || '')
|
const commit = this.isCurrentCommit(options.commit || '')
|
||||||
@ -295,7 +332,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: {
|
||||||
contains__image: {
|
release_image: {
|
||||||
$select: ['image'],
|
$select: ['image'],
|
||||||
$expand: {
|
$expand: {
|
||||||
image: {
|
image: {
|
||||||
|
@ -24,7 +24,7 @@ import { tryAsInteger } from '../../utils/validation';
|
|||||||
import { jsonInfo } from '../../utils/messages';
|
import { jsonInfo } from '../../utils/messages';
|
||||||
|
|
||||||
export const commitOrIdArg = Args.custom({
|
export const commitOrIdArg = Args.custom({
|
||||||
parse: async (commitOrId: string) => tryAsInteger(commitOrId),
|
parse: tryAsInteger,
|
||||||
});
|
});
|
||||||
|
|
||||||
type FlagsDef = Interfaces.InferredFlags<typeof ReleaseCmd.flags>;
|
type FlagsDef = Interfaces.InferredFlags<typeof ReleaseCmd.flags>;
|
||||||
@ -86,7 +86,7 @@ export default class ReleaseCmd extends Command {
|
|||||||
balena: BalenaSdk.BalenaSDK,
|
balena: BalenaSdk.BalenaSDK,
|
||||||
options: FlagsDef,
|
options: FlagsDef,
|
||||||
) {
|
) {
|
||||||
const fields: Array<keyof BalenaSdk.Release> = [
|
const fields = [
|
||||||
'id',
|
'id',
|
||||||
'commit',
|
'commit',
|
||||||
'created_at',
|
'created_at',
|
||||||
@ -96,7 +96,7 @@ export default class ReleaseCmd extends Command {
|
|||||||
'build_log',
|
'build_log',
|
||||||
'start_timestamp',
|
'start_timestamp',
|
||||||
'end_timestamp',
|
'end_timestamp',
|
||||||
];
|
] satisfies BalenaSdk.PineOptions<BalenaSdk.Release>['$select'];
|
||||||
|
|
||||||
const release = await balena.models.release.get(commitOrId, {
|
const release = await balena.models.release.get(commitOrId, {
|
||||||
...(!options.json && { $select: fields }),
|
...(!options.json && { $select: fields }),
|
||||||
|
@ -56,14 +56,14 @@ export default class ReleaseListCmd extends Command {
|
|||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(ReleaseListCmd);
|
const { args: params, flags: options } = await this.parse(ReleaseListCmd);
|
||||||
|
|
||||||
const fields: Array<keyof BalenaSdk.Release> = [
|
const fields = [
|
||||||
'id',
|
'id',
|
||||||
'commit',
|
'commit',
|
||||||
'created_at',
|
'created_at',
|
||||||
'status',
|
'status',
|
||||||
'semver',
|
'semver',
|
||||||
'is_final',
|
'is_final',
|
||||||
];
|
] satisfies BalenaSdk.PineOptions<BalenaSdk.Release>['$select'];
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const { getFleetSlug } = await import('../../utils/sdk');
|
const { getFleetSlug } = await import('../../utils/sdk');
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { Args, Command } from '@oclif/core';
|
||||||
import { ExpectedError } from '../../errors';
|
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
export default class SSHKeyAddCmd extends Command {
|
export default class SSHKeyAddCmd extends Command {
|
||||||
@ -59,6 +58,7 @@ export default class SSHKeyAddCmd extends Command {
|
|||||||
}),
|
}),
|
||||||
path: Args.string({
|
path: Args.string({
|
||||||
description: `the path to the public key file`,
|
description: `the path to the public key file`,
|
||||||
|
required: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,12 +67,12 @@ export default class SSHKeyAddCmd extends Command {
|
|||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(SSHKeyAddCmd);
|
const { args: params } = await this.parse(SSHKeyAddCmd);
|
||||||
|
|
||||||
|
const { readFile } = (await import('fs')).promises;
|
||||||
let key: string;
|
let key: string;
|
||||||
if (params.path != null) {
|
try {
|
||||||
const { readFile } = (await import('fs')).promises;
|
|
||||||
key = await readFile(params.path, 'utf8');
|
key = await readFile(params.path, 'utf8');
|
||||||
} else {
|
} catch {
|
||||||
throw new ExpectedError('No public key file or path provided.');
|
key = params.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
await getBalenaSdk().models.key.create(params.name, key);
|
await getBalenaSdk().models.key.create(params.name, key);
|
||||||
|
@ -34,7 +34,7 @@ export default class SSHKeyCmd extends Command {
|
|||||||
public static args = {
|
public static args = {
|
||||||
id: Args.integer({
|
id: Args.integer({
|
||||||
description: 'balenaCloud ID for the SSH key',
|
description: 'balenaCloud ID for the SSH key',
|
||||||
parse: async (x) => parseAsInteger(x, 'id'),
|
parse: (x) => parseAsInteger(x, 'id'),
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -40,7 +40,7 @@ export default class SSHKeyRmCmd extends Command {
|
|||||||
public static args = {
|
public static args = {
|
||||||
id: Args.integer({
|
id: Args.integer({
|
||||||
description: 'balenaCloud ID for the SSH key',
|
description: 'balenaCloud ID for the SSH key',
|
||||||
parse: async (x) => parseAsInteger(x, 'id'),
|
parse: (x) => parseAsInteger(x, 'id'),
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -69,10 +69,9 @@ export default class VersionCmd extends Command {
|
|||||||
const { flags: options } = await this.parse(VersionCmd);
|
const { flags: options } = await this.parse(VersionCmd);
|
||||||
const versions: JsonVersions = {
|
const versions: JsonVersions = {
|
||||||
'balena-cli': (await import('../../../package.json')).version,
|
'balena-cli': (await import('../../../package.json')).version,
|
||||||
'Node.js':
|
'Node.js': process.version.startsWith('v')
|
||||||
process.version && process.version.startsWith('v')
|
? process.version.slice(1)
|
||||||
? process.version.slice(1)
|
: process.version,
|
||||||
: process.version,
|
|
||||||
};
|
};
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
console.log(JSON.stringify(versions, null, 4));
|
console.log(JSON.stringify(versions, null, 4));
|
||||||
|
@ -93,7 +93,7 @@ function interpret(error: Error): string {
|
|||||||
|
|
||||||
if (hasCode(error)) {
|
if (hasCode(error)) {
|
||||||
const errorCodeHandler = messages[error.code];
|
const errorCodeHandler = messages[error.code];
|
||||||
const message = errorCodeHandler && errorCodeHandler(error);
|
const message = errorCodeHandler?.(error);
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
return message;
|
return message;
|
||||||
@ -229,7 +229,7 @@ async function sentryCaptureException(error: Error) {
|
|||||||
Sentry.captureException(error);
|
Sentry.captureException(error);
|
||||||
try {
|
try {
|
||||||
await Sentry.close(1000);
|
await Sentry.close(1000);
|
||||||
} catch (e) {
|
} catch {
|
||||||
if (process.env.DEBUG) {
|
if (process.env.DEBUG) {
|
||||||
console.error('[debug] Timeout reporting error to sentry.io');
|
console.error('[debug] Timeout reporting error to sentry.io');
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
Sentry.configureScope((scope) => {
|
scope = Sentry.getCurrentScope();
|
||||||
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,11 +52,9 @@ export async function trackCommand(commandSignature: string) {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
if (!process.env.BALENARC_NO_SENTRY) {
|
if (!process.env.BALENARC_NO_SENTRY) {
|
||||||
Sentry!.configureScope((scope) => {
|
scope!.setUser({
|
||||||
scope.setUser({
|
id: username,
|
||||||
id: username,
|
username,
|
||||||
username,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
@ -209,12 +209,12 @@ See: https://git.io/JRHUW#deprecation-policy`,
|
|||||||
return indent(body, 2);
|
return indent(body, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected formatDescription(desc: string = '') {
|
protected formatDescription(desc = '') {
|
||||||
const chalk = getChalk();
|
const chalk = getChalk();
|
||||||
|
|
||||||
desc = desc.split('\n')[0];
|
desc = desc.split('\n')[0];
|
||||||
// Remove any ending .
|
// Remove any ending .
|
||||||
if (desc[desc.length - 1] === '.') {
|
if (desc.endsWith('.')) {
|
||||||
desc = desc.substring(0, desc.length - 1);
|
desc = desc.substring(0, desc.length - 1);
|
||||||
}
|
}
|
||||||
// Lowercase first letter if second char is lowercase, to preserve e.g. 'SSH ...')
|
// Lowercase first letter if second char is lowercase, to preserve e.g. 'SSH ...')
|
||||||
|
@ -103,7 +103,7 @@ const hook: Hook<'prerun'> = async function (options) {
|
|||||||
.offlineCompatible ?? DEFAULT_OFFLINE_COMPATIBLE
|
.offlineCompatible ?? DEFAULT_OFFLINE_COMPATIBLE
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await checkNotUsingOfflineMode();
|
checkNotUsingOfflineMode();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error(error);
|
this.error(error);
|
||||||
|
@ -48,7 +48,7 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
|||||||
if (
|
if (
|
||||||
cmdSlice.length > 1 &&
|
cmdSlice.length > 1 &&
|
||||||
cmdSlice[0] === 'help' &&
|
cmdSlice[0] === 'help' &&
|
||||||
cmdSlice[1][0] !== '-'
|
!cmdSlice[1].startsWith('-')
|
||||||
) {
|
) {
|
||||||
cmdSlice.shift();
|
cmdSlice.shift();
|
||||||
cmdSlice.push('--help');
|
cmdSlice.push('--help');
|
||||||
|
@ -83,43 +83,6 @@ export function setOfflineModeEnvVars() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements the 'pkgExec' command, used as a way to provide a Node.js
|
|
||||||
* interpreter for child_process.spawn()-like operations when the CLI is
|
|
||||||
* executing as a standalone zip package (built-in Node interpreter) and
|
|
||||||
* the system may not have a separate Node.js installation. A present use
|
|
||||||
* case is a patched version of the 'windosu' package that requires a
|
|
||||||
* Node.js interpreter to spawn a privileged child process.
|
|
||||||
*
|
|
||||||
* @param modFunc Path to a JS module that will be executed via require().
|
|
||||||
* The modFunc argument may optionally contain a function name separated
|
|
||||||
* by '::', for example '::main' in:
|
|
||||||
* 'C:\\snapshot\\balena-cli\\node_modules\\windosu\\lib\\pipe.js::main'
|
|
||||||
* in which case that function is executed in the require'd module.
|
|
||||||
* @param args Optional arguments to passed through process.argv and as
|
|
||||||
* arguments to the function specified via modFunc.
|
|
||||||
*/
|
|
||||||
export async function pkgExec(modFunc: string, args: string[]) {
|
|
||||||
const [modPath, funcName] = modFunc.split('::');
|
|
||||||
let replacedModPath = modPath;
|
|
||||||
const match = modPath
|
|
||||||
.replace(/\\/g, '/')
|
|
||||||
.match(/\/snapshot\/balena-cli\/(.+)/);
|
|
||||||
if (match) {
|
|
||||||
replacedModPath = `../${match[1]}`;
|
|
||||||
}
|
|
||||||
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
||||||
try {
|
|
||||||
const mod: any = await import(replacedModPath);
|
|
||||||
if (funcName) {
|
|
||||||
await mod[funcName](...args);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error executing pkgExec "${modFunc}" [${args.join()}]`);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CachedUsername {
|
export interface CachedUsername {
|
||||||
token: string;
|
token: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -164,9 +164,8 @@ export async function downloadOSImage(
|
|||||||
stream.on('progress', (state: any) => {
|
stream.on('progress', (state: any) => {
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
return bar.update(state);
|
return bar.update(state);
|
||||||
} else {
|
|
||||||
return spinner.start();
|
|
||||||
}
|
}
|
||||||
|
spinner.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('end', () => {
|
stream.on('end', () => {
|
||||||
|
@ -128,6 +128,7 @@ 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');
|
||||||
@ -167,6 +168,7 @@ export const createRelease = async function (
|
|||||||
semver,
|
semver,
|
||||||
is_final: !draft,
|
is_final: !draft,
|
||||||
contract,
|
contract,
|
||||||
|
imgDescriptors,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -240,7 +242,7 @@ export const getPreviousRepos = (
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
},
|
},
|
||||||
$expand: {
|
$expand: {
|
||||||
contains__image: {
|
release_image: {
|
||||||
$select: 'image',
|
$select: 'image',
|
||||||
$expand: { image: { $select: 'is_stored_at__image_location' } },
|
$expand: { image: { $select: 'is_stored_at__image_location' } },
|
||||||
},
|
},
|
||||||
@ -252,7 +254,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].contains__image as Array<{
|
const images = release[0].release_image as Array<{
|
||||||
image: [SDK.Image];
|
image: [SDK.Image];
|
||||||
}>;
|
}>;
|
||||||
const { getRegistryAndName } =
|
const { getRegistryAndName } =
|
||||||
@ -386,7 +388,7 @@ export class BuildProgressUI implements Renderer {
|
|||||||
.map(function (service) {
|
.map(function (service) {
|
||||||
const stream = through.obj(function (event, _enc, cb) {
|
const stream = through.obj(function (event, _enc, cb) {
|
||||||
eventHandler(service, event);
|
eventHandler(service, event);
|
||||||
return cb();
|
cb();
|
||||||
});
|
});
|
||||||
stream.pipe(tty.stream, { end: false });
|
stream.pipe(tty.stream, { end: false });
|
||||||
return [service, stream];
|
return [service, stream];
|
||||||
@ -471,17 +473,20 @@ export class BuildProgressUI implements Renderer {
|
|||||||
const { status, progress, error } = serviceToDataMap[service] ?? {};
|
const { status, progress, error } = serviceToDataMap[service] ?? {};
|
||||||
if (error) {
|
if (error) {
|
||||||
return `${error}`;
|
return `${error}`;
|
||||||
} else if (progress) {
|
}
|
||||||
|
|
||||||
|
if (progress) {
|
||||||
const bar = renderProgressBar(progress, 20);
|
const bar = renderProgressBar(progress, 20);
|
||||||
if (status) {
|
if (status) {
|
||||||
return `${bar} ${status}`;
|
return `${bar} ${status}`;
|
||||||
}
|
}
|
||||||
return `${bar}`;
|
return bar;
|
||||||
} else if (status) {
|
|
||||||
return `${status}`;
|
|
||||||
} else {
|
|
||||||
return 'Waiting...';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
return 'Waiting...';
|
||||||
})
|
})
|
||||||
.map((data, index) => [services[index], data])
|
.map((data, index) => [services[index], data])
|
||||||
.fromPairs()
|
.fromPairs()
|
||||||
@ -552,7 +557,7 @@ export class BuildProgressInline implements Renderer {
|
|||||||
.map(function (service) {
|
.map(function (service) {
|
||||||
const stream = through.obj(function (event, _enc, cb) {
|
const stream = through.obj(function (event, _enc, cb) {
|
||||||
eventHandler(service, event);
|
eventHandler(service, event);
|
||||||
return cb();
|
cb();
|
||||||
});
|
});
|
||||||
stream.pipe(outStream, { end: false });
|
stream.pipe(outStream, { end: false });
|
||||||
return [service, stream];
|
return [service, stream];
|
||||||
@ -606,11 +611,11 @@ export class BuildProgressInline implements Renderer {
|
|||||||
const { status, error } = event;
|
const { status, error } = event;
|
||||||
if (error) {
|
if (error) {
|
||||||
return `${error}`;
|
return `${error}`;
|
||||||
} else if (status) {
|
|
||||||
return `${status}`;
|
|
||||||
} else {
|
|
||||||
return 'Waiting...';
|
|
||||||
}
|
}
|
||||||
|
if (status) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
return 'Waiting...';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const prefix = _.padEnd(getChalk().bold(service), this._prefixWidth);
|
const prefix = _.padEnd(getChalk().bold(service), this._prefixWidth);
|
||||||
|
@ -966,7 +966,7 @@ export async function makeBuildTasks(
|
|||||||
deviceInfo: DeviceInfo,
|
deviceInfo: DeviceInfo,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
projectName: string,
|
projectName: string,
|
||||||
releaseHash: string = 'unavailable',
|
releaseHash = 'unavailable',
|
||||||
preprocessHook?: (dockerfile: string) => string,
|
preprocessHook?: (dockerfile: string) => string,
|
||||||
): Promise<MultiBuild.BuildTask[]> {
|
): Promise<MultiBuild.BuildTask[]> {
|
||||||
const multiBuild = await import('@balena/compose/dist/multibuild');
|
const multiBuild = await import('@balena/compose/dist/multibuild');
|
||||||
@ -1322,6 +1322,9 @@ 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,
|
||||||
@ -1344,23 +1347,34 @@ async function pushServiceImages(
|
|||||||
delete serviceImage.build_log;
|
delete serviceImage.build_log;
|
||||||
}
|
}
|
||||||
|
|
||||||
await releaseMod.updateImage(
|
// These are the only update-able image fields in bC atm, and passing
|
||||||
pineClient,
|
// the whole image object in v7+ would result the allowlist to reject the request.
|
||||||
serviceImage.id,
|
const imagePayload = _.pick(serviceImage, [
|
||||||
// These are the only update-able image fields in bC atm, and passing
|
'end_timestamp',
|
||||||
// the whole image object in v7+ would result the allowlist to reject the request.
|
'project_type',
|
||||||
_.pick(serviceImage, [
|
'error_message',
|
||||||
'end_timestamp',
|
'build_log',
|
||||||
'project_type',
|
'push_timestamp',
|
||||||
'error_message',
|
'status',
|
||||||
'build_log',
|
'content_hash',
|
||||||
'push_timestamp',
|
'dockerfile',
|
||||||
'status',
|
'image_size',
|
||||||
'content_hash',
|
]);
|
||||||
'dockerfile',
|
|
||||||
'image_size',
|
if (
|
||||||
]),
|
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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1375,6 +1389,7 @@ 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');
|
||||||
@ -1405,6 +1420,7 @@ 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;
|
||||||
@ -1492,7 +1508,7 @@ export function createRunLoop(tick: (...args: any[]) => void) {
|
|||||||
},
|
},
|
||||||
end() {
|
end() {
|
||||||
clearInterval(timerId);
|
clearInterval(timerId);
|
||||||
return runloop.onEnd();
|
runloop.onEnd();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return runloop;
|
return runloop;
|
||||||
@ -1549,7 +1565,7 @@ function dropEmptyLinesStream() {
|
|||||||
if (str.trim()) {
|
if (str.trim()) {
|
||||||
this.push(str);
|
this.push(str);
|
||||||
}
|
}
|
||||||
return cb();
|
cb();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1570,7 +1586,7 @@ function buildLogCapture(objectMode: boolean, buffer: string[]) {
|
|||||||
buffer.push(data);
|
buffer.push(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(null, data);
|
cb(null, data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1585,13 +1601,17 @@ function buildProgressAdapter(inline: boolean) {
|
|||||||
|
|
||||||
return through({ objectMode: true }, function (str, _enc, cb) {
|
return through({ objectMode: true }, function (str, _enc, cb) {
|
||||||
if (str == null) {
|
if (str == null) {
|
||||||
return cb(null, str);
|
cb(null, str);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inline) {
|
if (inline) {
|
||||||
return cb(null, { status: str });
|
cb(null, { status: str });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We want to keep the regex match instead of startsWith as it also works with buffers
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
|
||||||
if (!/^Successfully tagged /.test(str)) {
|
if (!/^Successfully tagged /.test(str)) {
|
||||||
const match = stepRegex.exec(str);
|
const match = stepRegex.exec(str);
|
||||||
if (match) {
|
if (match) {
|
||||||
@ -1607,7 +1627,7 @@ function buildProgressAdapter(inline: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(null, { status: str, progress });
|
cb(null, { status: str, progress });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@ 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 {
|
||||||
@ -81,7 +80,7 @@ export async function generateApplicationConfig(
|
|||||||
)) as ImgConfig;
|
)) as ImgConfig;
|
||||||
|
|
||||||
// merge sshKeys to config, when they have been specified
|
// merge sshKeys to config, when they have been specified
|
||||||
if (options.os && options.os.sshKeys) {
|
if (options.os?.sshKeys) {
|
||||||
// Create config.os object if it does not exist
|
// Create config.os object if it does not exist
|
||||||
config.os = config.os ? config.os : {};
|
config.os = config.os ? config.os : {};
|
||||||
config.os.sshKeys = config.os.sshKeys
|
config.os.sshKeys = config.os.sshKeys
|
||||||
@ -122,16 +121,10 @@ 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;
|
||||||
|
|
||||||
if (deviceApiKey == null && semver.satisfies(options.version, '<2.0.3')) {
|
config.deviceApiKey =
|
||||||
config.apiKey = await sdk.models.application.generateApiKey(
|
typeof deviceApiKey === 'string' && deviceApiKey
|
||||||
application.id,
|
? deviceApiKey
|
||||||
);
|
: 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 { Request } from 'request';
|
import type got from 'got';
|
||||||
|
|
||||||
const getBuilderPushEndpoint = function (
|
const getBuilderPushEndpoint = function (
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
@ -75,7 +75,10 @@ const showPushProgress = function (message: string) {
|
|||||||
return progressBar;
|
return progressBar;
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
|
const uploadToPromise = (
|
||||||
|
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;
|
||||||
@ -86,7 +89,7 @@ const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
|
|||||||
obj = JSON.parse(data);
|
obj = JSON.parse(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.logError('Error parsing reply from remote side');
|
logger.logError('Error parsing reply from remote side');
|
||||||
reject(e);
|
reject(e as Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,10 +109,7 @@ const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
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,10 +117,9 @@ const uploadImage = function (
|
|||||||
appName: string,
|
appName: string,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<{ buildId: number }> {
|
): Promise<{ buildId: number }> {
|
||||||
const request = require('request') as typeof import('request');
|
const { default: got } = await import('got');
|
||||||
const progressStream =
|
const progressStream = await import('progress-stream');
|
||||||
require('progress-stream') as typeof import('progress-stream');
|
const zlib = await import('zlib');
|
||||||
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
|
||||||
@ -141,25 +140,26 @@ const uploadImage = function (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const uploadRequest = request.post({
|
const uploadRequest = got.stream.post(
|
||||||
url: getBuilderPushEndpoint(url, username, appName),
|
getBuilderPushEndpoint(url, username, appName),
|
||||||
headers: {
|
{
|
||||||
'Content-Encoding': 'gzip',
|
headers: {
|
||||||
|
'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 = function (
|
const uploadLogs = async function (
|
||||||
logs: string,
|
logs: string,
|
||||||
token: string,
|
token: string,
|
||||||
url: string,
|
url: string,
|
||||||
@ -167,14 +167,14 @@ const uploadLogs = function (
|
|||||||
username: string,
|
username: string,
|
||||||
appName: string,
|
appName: string,
|
||||||
) {
|
) {
|
||||||
const request = require('request') as typeof import('request');
|
const { default: got } = await import('got');
|
||||||
return request.post({
|
return got.post(getBuilderLogPushEndpoint(url, buildId, username, appName), {
|
||||||
json: true,
|
headers: {
|
||||||
url: getBuilderLogPushEndpoint(url, buildId, username, appName),
|
Authorization: `Bearer ${token}`,
|
||||||
auth: {
|
|
||||||
bearer: token,
|
|
||||||
},
|
},
|
||||||
body: Buffer.from(logs),
|
body: Buffer.from(logs),
|
||||||
|
responseType: 'json',
|
||||||
|
throwHttpErrors: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
@ -74,15 +74,15 @@ export class DeviceAPI {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private logger: Logger,
|
private logger: Logger,
|
||||||
addr: string,
|
addr: string,
|
||||||
port: number = 48484,
|
port = 48484,
|
||||||
) {
|
) {
|
||||||
this.deviceAddress = `http://${addr}:${port}/`;
|
this.deviceAddress = `http://${addr}:${port}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either return nothing, or throw an error with the info
|
// Either return nothing, or throw an error with the info
|
||||||
public async setTargetState(state: any): Promise<void> {
|
public async setTargetState(state: Record<string, any>) {
|
||||||
const url = this.getUrlForAction('setTargetState');
|
const url = this.getUrlForAction('setTargetState');
|
||||||
return DeviceAPI.promisifiedRequest(
|
await DeviceAPI.sendRequest(
|
||||||
{
|
{
|
||||||
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 DeviceAPI.promisifiedRequest(
|
return await DeviceAPI.sendRequest(
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
json: true,
|
json: true,
|
||||||
},
|
},
|
||||||
this.logger,
|
this.logger,
|
||||||
).then((body) => {
|
).then(({ state }: { state: Record<string, any> }) => {
|
||||||
return body.state;
|
return state;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDeviceInformation(): Promise<DeviceInfo> {
|
public async getDeviceInformation() {
|
||||||
const url = this.getUrlForAction('getDeviceInformation');
|
const url = this.getUrlForAction('getDeviceInformation');
|
||||||
|
|
||||||
return DeviceAPI.promisifiedRequest(
|
return await DeviceAPI.sendRequest(
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
json: true,
|
json: true,
|
||||||
},
|
},
|
||||||
this.logger,
|
this.logger,
|
||||||
).then((body) => {
|
).then(({ info }: { info: DeviceInfo }) => {
|
||||||
return body.info;
|
return 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.promisifiedRequest(
|
const body = await DeviceAPI.sendRequest(
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
@ -146,10 +146,10 @@ export class DeviceAPI {
|
|||||||
return body.containerId;
|
return body.containerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ping(): Promise<void> {
|
public async ping() {
|
||||||
const url = this.getUrlForAction('ping');
|
const url = this.getUrlForAction('ping');
|
||||||
|
|
||||||
return DeviceAPI.promisifiedRequest(
|
await DeviceAPI.sendRequest(
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
@ -158,10 +158,10 @@ export class DeviceAPI {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getVersion(): Promise<string> {
|
public async getVersion(): Promise<string> {
|
||||||
const url = this.getUrlForAction('version');
|
const url = this.getUrlForAction('version');
|
||||||
|
|
||||||
return DeviceAPI.promisifiedRequest({
|
return await DeviceAPI.sendRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
json: true,
|
json: true,
|
||||||
@ -176,10 +176,10 @@ export class DeviceAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStatus(): Promise<Status> {
|
public async getStatus() {
|
||||||
const url = this.getUrlForAction('status');
|
const url = this.getUrlForAction('status');
|
||||||
|
|
||||||
return DeviceAPI.promisifiedRequest({
|
return await DeviceAPI.sendRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
json: true,
|
json: true,
|
||||||
@ -194,91 +194,60 @@ export class DeviceAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLogStream(): Promise<Stream.Readable> {
|
public async getLogStream() {
|
||||||
const url = this.getUrlForAction('logs');
|
const url = this.getUrlForAction('logs');
|
||||||
|
const sdk = getBalenaSdk();
|
||||||
|
|
||||||
// Don't use the promisified version here as we want to stream the output
|
const stream = await sdk.request.stream({ url });
|
||||||
return new Promise((resolve, reject) => {
|
stream.on('response', (res) => {
|
||||||
const req = request.get(url);
|
if (res.statusCode !== 200) {
|
||||||
|
throw new ApiErrors.DeviceAPIError(
|
||||||
req.on('error', reject).on('response', async (res) => {
|
'Non-200 response from log streaming endpoint',
|
||||||
if (res.statusCode !== 200) {
|
);
|
||||||
reject(
|
}
|
||||||
new ApiErrors.DeviceAPIError(
|
res.socket.setKeepAlive(true, 1000);
|
||||||
'Non-200 response from log streaming endpoint',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
res.socket.setKeepAlive(true, 1000);
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
resolve(res);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUrlForAction(action: keyof typeof deviceEndpoints): string {
|
private getUrlForAction(action: keyof typeof deviceEndpoints) {
|
||||||
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 promisifiedRequest<
|
private static async sendRequest(
|
||||||
T extends Parameters<typeof request>[0],
|
opts: Parameters<BalenaSDK['request']['send']>[number],
|
||||||
>(opts: T, logger?: Logger): Promise<any> {
|
logger?: Logger,
|
||||||
interface ObjectWithUrl {
|
) {
|
||||||
url?: string;
|
if (logger != null && opts.url != null) {
|
||||||
}
|
logger.logDebug(`Sending request to ${opts.url}`);
|
||||||
|
|
||||||
if (logger != null) {
|
|
||||||
let url: string | null = null;
|
|
||||||
if (_.isObject(opts) && (opts as ObjectWithUrl).url != null) {
|
|
||||||
// the `as string` shouldn't be necessary, but the type system
|
|
||||||
// is getting a little confused
|
|
||||||
url = (opts as ObjectWithUrl).url as string;
|
|
||||||
} else if (typeof opts === 'string') {
|
|
||||||
url = opts;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url != null) {
|
|
||||||
logger.logDebug(`Sending request to ${url}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sdk = getBalenaSdk();
|
||||||
const doRequest = async () => {
|
const doRequest = async () => {
|
||||||
return await new Promise((resolve, reject) => {
|
const response = await sdk.request.send(opts);
|
||||||
return request(opts, (err, response, body) => {
|
const bodyError =
|
||||||
if (err) {
|
typeof response.body === 'string'
|
||||||
return reject(err);
|
? response.body
|
||||||
}
|
: response.body.message;
|
||||||
switch (response.statusCode) {
|
switch (response.statusCode) {
|
||||||
case 200:
|
case 200:
|
||||||
return resolve(body);
|
return response.body;
|
||||||
case 400:
|
case 400:
|
||||||
return reject(
|
throw new ApiErrors.BadRequestDeviceAPIError(bodyError);
|
||||||
new ApiErrors.BadRequestDeviceAPIError(body.message),
|
case 503:
|
||||||
);
|
throw new ApiErrors.ServiceUnavailableAPIError(bodyError);
|
||||||
case 503:
|
default:
|
||||||
return reject(
|
new ApiErrors.DeviceAPIError(bodyError);
|
||||||
new ApiErrors.ServiceUnavailableAPIError(body.message),
|
}
|
||||||
);
|
|
||||||
default:
|
|
||||||
return reject(new ApiErrors.DeviceAPIError(body.message));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return await retry({
|
return await retry({
|
||||||
func: doRequest,
|
func: doRequest,
|
||||||
initialDelayMs: 2000,
|
initialDelayMs: 2000,
|
||||||
maxAttempts: 6,
|
maxAttempts: 6,
|
||||||
label: `Supervisor API (${opts.method} ${(opts as any).url})`,
|
label: `Supervisor API (${opts.method} ${opts.url})`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeviceAPI;
|
|
||||||
|
@ -74,11 +74,11 @@ interface ParsedEnvironment {
|
|||||||
[serviceName: string]: { [key: string]: string };
|
[serviceName: string]: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function environmentFromInput(
|
function environmentFromInput(
|
||||||
envs: string[],
|
envs: string[],
|
||||||
serviceNames: string[],
|
serviceNames: string[],
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<ParsedEnvironment> {
|
): ParsedEnvironment {
|
||||||
// A normal environment variable regex, with an added part
|
// A normal environment variable regex, with an added part
|
||||||
// to find a colon followed servicename at the start
|
// to find a colon followed servicename at the start
|
||||||
const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/;
|
const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/;
|
||||||
@ -143,7 +143,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
globalLogger.logDebug('Checking we can access device');
|
globalLogger.logDebug('Checking we can access device');
|
||||||
await api.ping();
|
await api.ping();
|
||||||
} catch (e) {
|
} catch {
|
||||||
throw new ExpectedError(stripIndent`
|
throw new ExpectedError(stripIndent`
|
||||||
Could not communicate with device supervisor at address ${opts.deviceHost}:${port}.
|
Could not communicate with device supervisor at address ${opts.deviceHost}:${port}.
|
||||||
Device may not have local mode enabled. Check with:
|
Device may not have local mode enabled. Check with:
|
||||||
@ -191,10 +191,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Attempt to attach to the device's docker daemon
|
// Attempt to attach to the device's docker daemon
|
||||||
const docker = connectToDocker(
|
const docker = connectToDocker(opts.deviceHost, opts.devicePort ?? 2375);
|
||||||
opts.deviceHost,
|
|
||||||
opts.devicePort != null ? opts.devicePort : 2375,
|
|
||||||
);
|
|
||||||
|
|
||||||
await checkBuildSecretsRequirements(docker, opts.source);
|
await checkBuildSecretsRequirements(docker, opts.source);
|
||||||
globalLogger.logDebug('Tarring all non-ignored files...');
|
globalLogger.logDebug('Tarring all non-ignored files...');
|
||||||
@ -231,7 +228,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
|||||||
// Print a newline to clearly separate build time and runtime
|
// Print a newline to clearly separate build time and runtime
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
const envs = await environmentFromInput(
|
const envs = environmentFromInput(
|
||||||
opts.env,
|
opts.env,
|
||||||
Object.getOwnPropertyNames(project.composition.services),
|
Object.getOwnPropertyNames(project.composition.services),
|
||||||
globalLogger,
|
globalLogger,
|
||||||
@ -388,7 +385,7 @@ async function performBuilds(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Check for failures
|
// Check for failures
|
||||||
await inspectBuildResults(localImages);
|
inspectBuildResults(localImages);
|
||||||
|
|
||||||
const imagesToRemove: string[] = [];
|
const imagesToRemove: string[] = [];
|
||||||
|
|
||||||
@ -497,7 +494,7 @@ export async function rebuildSingleTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await assignDockerBuildOpts(docker, [task], opts);
|
await assignDockerBuildOpts(docker, [task], opts);
|
||||||
await assignOutputHandlers([task], logger, logHandler);
|
assignOutputHandlers([task], logger, logHandler);
|
||||||
|
|
||||||
const [localImage] = await multibuild.performBuilds(
|
const [localImage] = await multibuild.performBuilds(
|
||||||
[task],
|
[task],
|
||||||
@ -568,7 +565,7 @@ async function assignDockerBuildOpts(
|
|||||||
globalLogger.logDebug(`Using ${images.length} on-device images for cache...`);
|
globalLogger.logDebug(`Using ${images.length} on-device images for cache...`);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
buildTasks.map(async (task: BuildTask) => {
|
buildTasks.map((task: BuildTask) => {
|
||||||
task.dockerOpts = {
|
task.dockerOpts = {
|
||||||
...(task.dockerOpts || {}),
|
...(task.dockerOpts || {}),
|
||||||
...{
|
...{
|
||||||
@ -606,11 +603,11 @@ function getImageNameFromTask(task: BuildTask): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateTargetState(
|
export function generateTargetState(
|
||||||
currentTargetState: any,
|
currentTargetState: Record<string, 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 } = {};
|
||||||
@ -666,7 +663,7 @@ export function generateTargetState(
|
|||||||
return targetState;
|
return targetState;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function inspectBuildResults(images: LocalImage[]): Promise<void> {
|
function inspectBuildResults(images: LocalImage[]): void {
|
||||||
const failures: LocalPushErrors.BuildFailure[] = [];
|
const failures: LocalPushErrors.BuildFailure[] = [];
|
||||||
|
|
||||||
_.each(images, (image) => {
|
_.each(images, (image) => {
|
||||||
@ -679,6 +676,6 @@ async function inspectBuildResults(images: LocalImage[]): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
throw new LocalPushErrors.BuildError(failures).toString();
|
throw new LocalPushErrors.BuildError(failures);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
@ -191,8 +191,8 @@ export class LivepushManager {
|
|||||||
);
|
);
|
||||||
const eventQueue = this.updateEventsWaiting[$serviceName];
|
const eventQueue = this.updateEventsWaiting[$serviceName];
|
||||||
eventQueue.push(changedPath);
|
eventQueue.push(changedPath);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.getDebouncedEventHandler($serviceName)();
|
void this.getDebouncedEventHandler($serviceName)();
|
||||||
};
|
};
|
||||||
|
|
||||||
const monitor = this.setupFilesystemWatcher(
|
const monitor = this.setupFilesystemWatcher(
|
||||||
@ -252,7 +252,7 @@ export class LivepushManager {
|
|||||||
try {
|
try {
|
||||||
// sync because chokidar defines a sync interface
|
// sync because chokidar defines a sync interface
|
||||||
stats = fs.lstatSync(filePath);
|
stats = fs.lstatSync(filePath);
|
||||||
} catch (err) {
|
} catch {
|
||||||
// OK: the file may have been deleted. See also:
|
// OK: the file may have been deleted. See also:
|
||||||
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/fsevents-handler.js#L326-L328
|
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/fsevents-handler.js#L326-L328
|
||||||
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/nodefs-handler.js#L364
|
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/nodefs-handler.js#L364
|
||||||
@ -267,15 +267,15 @@ export class LivepushManager {
|
|||||||
return dockerignore.ignores(relPath);
|
return dockerignore.ignores(relPath);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
monitor.on('add', (changedPath: string) =>
|
monitor.on('add', (changedPath: string) => {
|
||||||
changedPathHandler(serviceName, changedPath),
|
changedPathHandler(serviceName, changedPath);
|
||||||
);
|
});
|
||||||
monitor.on('change', (changedPath: string) =>
|
monitor.on('change', (changedPath: string) => {
|
||||||
changedPathHandler(serviceName, changedPath),
|
changedPathHandler(serviceName, changedPath);
|
||||||
);
|
});
|
||||||
monitor.on('unlink', (changedPath: string) =>
|
monitor.on('unlink', (changedPath: string) => {
|
||||||
changedPathHandler(serviceName, changedPath),
|
changedPathHandler(serviceName, changedPath);
|
||||||
);
|
});
|
||||||
return monitor;
|
return monitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ export const dockerConnectionCliFlags = {
|
|||||||
description:
|
description:
|
||||||
'Docker daemon TCP port number (hint: 2375 for balena devices)',
|
'Docker daemon TCP port number (hint: 2375 for balena devices)',
|
||||||
char: 'p',
|
char: 'p',
|
||||||
parse: async (p) => parseAsInteger(p, 'dockerPort'),
|
parse: (p) => parseAsInteger(p, 'dockerPort'),
|
||||||
}),
|
}),
|
||||||
ca: Flags.string({
|
ca: Flags.string({
|
||||||
description: 'Docker host TLS certificate authority file',
|
description: 'Docker host TLS certificate authority file',
|
||||||
@ -169,9 +169,7 @@ export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
|
|||||||
// version of balenaEngine, but it was at one point (mis)spelt 'balaena':
|
// version of balenaEngine, but it was at one point (mis)spelt 'balaena':
|
||||||
// https://github.com/balena-os/balena-engine/pull/32/files
|
// https://github.com/balena-os/balena-engine/pull/32/files
|
||||||
const dockerVersion = (await docker.version()) as BalenaEngineVersion;
|
const dockerVersion = (await docker.version()) as BalenaEngineVersion;
|
||||||
return !!(
|
return !!dockerVersion.Engine?.match(/balena|balaena/);
|
||||||
dockerVersion.Engine && dockerVersion.Engine.match(/balena|balaena/)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDocker(
|
export async function getDocker(
|
||||||
|
@ -84,7 +84,7 @@ export async function readFileWithEolConversion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Analyse encoding
|
// Analyse encoding
|
||||||
const encoding = await detectEncoding(fileBuffer);
|
const encoding = detectEncoding(fileBuffer);
|
||||||
|
|
||||||
// Skip further processing of non-convertible encodings
|
// Skip further processing of non-convertible encodings
|
||||||
if (!CONVERTIBLE_ENCODINGS.includes(encoding)) {
|
if (!CONVERTIBLE_ENCODINGS.includes(encoding)) {
|
||||||
@ -132,10 +132,10 @@ export async function readFileWithEolConversion(
|
|||||||
* @param fileBuffer File contents whose encoding should be detected
|
* @param fileBuffer File contents whose encoding should be detected
|
||||||
* @param bytesRead Optional "file size" if smaller than the buffer size
|
* @param bytesRead Optional "file size" if smaller than the buffer size
|
||||||
*/
|
*/
|
||||||
export async function detectEncoding(
|
export function detectEncoding(
|
||||||
fileBuffer: Buffer,
|
fileBuffer: Buffer,
|
||||||
bytesRead = fileBuffer.length,
|
bytesRead = fileBuffer.length,
|
||||||
): Promise<string> {
|
): string {
|
||||||
// empty file
|
// empty file
|
||||||
if (bytesRead === 0) {
|
if (bytesRead === 0) {
|
||||||
return '';
|
return '';
|
||||||
|
@ -110,6 +110,27 @@ 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 &&
|
||||||
@ -281,8 +302,7 @@ export function isWindowsComExeShell() {
|
|||||||
// neither bash nor sh (e.g. not MSYS, MSYS2, Cygwin, WSL)
|
// neither bash nor sh (e.g. not MSYS, MSYS2, Cygwin, WSL)
|
||||||
process.env.SHELL == null &&
|
process.env.SHELL == null &&
|
||||||
// Windows cmd.exe or PowerShell
|
// Windows cmd.exe or PowerShell
|
||||||
process.env.ComSpec != null &&
|
process.env.ComSpec?.endsWith('cmd.exe')
|
||||||
process.env.ComSpec.endsWith('cmd.exe')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,7 +386,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
|
|||||||
let url: InstanceType<typeof URL>;
|
let url: InstanceType<typeof URL>;
|
||||||
try {
|
try {
|
||||||
url = new URL(proxyUrl);
|
url = new URL(proxyUrl);
|
||||||
} catch (_e) {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -469,7 +489,7 @@ export function pickAndRename<T extends Dictionary<any>>(
|
|||||||
let renameFrom = f;
|
let renameFrom = f;
|
||||||
let renameTo = f;
|
let renameTo = f;
|
||||||
const match = f.match(/(?<from>\S+)\s+=>\s+(?<to>\S+)/);
|
const match = f.match(/(?<from>\S+)\s+=>\s+(?<to>\S+)/);
|
||||||
if (match && match.groups) {
|
if (match?.groups) {
|
||||||
renameFrom = match.groups.from;
|
renameFrom = match.groups.from;
|
||||||
renameTo = match.groups.to;
|
renameTo = match.groups.to;
|
||||||
}
|
}
|
||||||
|
69
src/utils/image-contents.ts
Normal file
69
src/utils/image-contents.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Balena Ltd.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Utilities to explore the contents in a balenaOS image.
|
||||||
|
|
||||||
|
import * as imagefs from 'balena-image-fs';
|
||||||
|
import * as filedisk from 'file-disk';
|
||||||
|
import { getPartitions } from 'partitioninfo';
|
||||||
|
import type * as Fs from 'fs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary IDs for the standard balenaOS partitions
|
||||||
|
* @description Values are the base name for a partition on disk
|
||||||
|
*/
|
||||||
|
export enum BalenaPartition {
|
||||||
|
BOOT = 'boot',
|
||||||
|
ROOTA = 'rootA',
|
||||||
|
ROOTB = 'rootB',
|
||||||
|
STATE = 'state',
|
||||||
|
DATA = 'data',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Allow a provided function to explore the contents of one of the well-known
|
||||||
|
* partitions of a balenaOS image
|
||||||
|
*
|
||||||
|
* @param {string} imagePath - pathname of image for search
|
||||||
|
* @param {BalenaPartition} partitionId - partition to find
|
||||||
|
* @param {(fs) => Promise<T>} - function for exploration
|
||||||
|
* @returns {T}
|
||||||
|
*/
|
||||||
|
export async function explorePartition<T>(
|
||||||
|
imagePath: string,
|
||||||
|
partitionId: BalenaPartition,
|
||||||
|
exploreFn: (fs: typeof Fs) => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
return await filedisk.withOpenFile(imagePath, 'r', async (handle) => {
|
||||||
|
const disk = new filedisk.FileDisk(handle, true, false, false);
|
||||||
|
const partitionInfo = await getPartitions(disk, {
|
||||||
|
includeExtended: false,
|
||||||
|
getLogical: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const findResult = await imagefs.findPartition(disk, partitionInfo, [
|
||||||
|
`resin-${partitionId}`,
|
||||||
|
`flash-${partitionId}`,
|
||||||
|
`balena-${partitionId}`,
|
||||||
|
]);
|
||||||
|
if (findResult == null) {
|
||||||
|
throw new Error(`Can't find partition for ${partitionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await imagefs.interact<T>(disk, findResult.index, exploreFn);
|
||||||
|
});
|
||||||
|
}
|
@ -76,38 +76,28 @@ export const getImagePath = async (deviceType: string, version?: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Determine if a device image is fresh
|
* @summary Determine if a device image is cached
|
||||||
*
|
*
|
||||||
* @description
|
* @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 fresh
|
* @returns {Promise<Boolean>} is image cached
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* isImageFresh('raspberry-pi', '1.2.3').then (isFresh) ->
|
* isImageCached ('raspberry-pi', '1.2.3').then (isCached) ->
|
||||||
* if isFresh
|
* if isCached
|
||||||
* console.log('The Raspberry Pi image v1.2.3 is fresh!')
|
* console.log('The Raspberry Pi image v1.2.3 is cached!')
|
||||||
*/
|
*/
|
||||||
export const isImageFresh = async (deviceType: string, version: string) => {
|
export const isImageCached = async (deviceType: string, version: string) => {
|
||||||
const imagePath = await getImagePath(deviceType, version);
|
const imagePath = await getImagePath(deviceType, version);
|
||||||
let createdDate;
|
|
||||||
try {
|
try {
|
||||||
createdDate = await getFileCreatedDate(imagePath);
|
const 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,7 +108,7 @@ export const isImageFresh = async (deviceType: string, version: string) => {
|
|||||||
*/
|
*/
|
||||||
export const isESR = (version: string) => {
|
export const isESR = (version: string) => {
|
||||||
const match = version.match(/^v?(\d+)\.\d+\.\d+/);
|
const match = version.match(/^v?(\d+)\.\d+\.\d+/);
|
||||||
const major = parseInt((match && match[1]) || '', 10);
|
const major = parseInt(match?.[1] || '', 10);
|
||||||
return major >= 2018; // note: (NaN >= 2018) is false
|
return major >= 2018; // note: (NaN >= 2018) is false
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -286,7 +276,7 @@ export const getStream = async (
|
|||||||
versionOrRange = 'latest';
|
versionOrRange = 'latest';
|
||||||
}
|
}
|
||||||
const version = await resolveVersion(deviceType, versionOrRange);
|
const version = await resolveVersion(deviceType, versionOrRange);
|
||||||
const isFresh = await isImageFresh(deviceType, version);
|
const isFresh = await isImageCached(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,6 +21,7 @@ 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) => {
|
||||||
@ -43,9 +44,26 @@ export const onceAsync = <T>(fn: () => Promise<T>) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBalenaSdk = once(() =>
|
const cliXBalenaClientHeaderInterceptor: BalenaSdk.Interceptor = {
|
||||||
(require('balena-sdk') as typeof BalenaSdk).fromSharedOptions(),
|
request($request) {
|
||||||
);
|
if ($request.headers['X-Balena-Client']) {
|
||||||
|
// We intentionally overwrite the sdk version string from the header
|
||||||
|
// to conserve bandwidth. We only do that when the SDK already has specified
|
||||||
|
// the X-Balena-Client header, since that signals that this is a safe url to
|
||||||
|
// include the extra header and will not cause CORS errors.
|
||||||
|
$request.headers['X-Balena-Client'] = `balena-cli/${version}`;
|
||||||
|
}
|
||||||
|
return $request;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBalenaSdk = once(() => {
|
||||||
|
const sdk = (require('balena-sdk') as typeof BalenaSdk).fromSharedOptions();
|
||||||
|
if (!sdk.interceptors.includes(cliXBalenaClientHeaderInterceptor)) {
|
||||||
|
sdk.interceptors.push(cliXBalenaClientHeaderInterceptor);
|
||||||
|
}
|
||||||
|
return sdk;
|
||||||
|
});
|
||||||
|
|
||||||
export const getVisuals = once(
|
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 anauthenticated root ssh access and exposing network ports such as
|
allowing unauthenticated 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 partion. Development
|
to be inserted in the 'config.json' file in the image's boot partition. 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
|
||||||
|
@ -39,7 +39,7 @@ export async function disambiguateReleaseParam(
|
|||||||
|
|
||||||
// Accepting short hashes of 7,8,9 chars.
|
// Accepting short hashes of 7,8,9 chars.
|
||||||
const possibleUuidHashLength = [7, 8, 9, 32, 40, 62].includes(release.length);
|
const possibleUuidHashLength = [7, 8, 9, 32, 40, 62].includes(release.length);
|
||||||
const hasLeadingZero = release[0] === '0';
|
const hasLeadingZero = release.startsWith('0');
|
||||||
const isOnlyNumerical = /^[0-9]+$/.test(release);
|
const isOnlyNumerical = /^[0-9]+$/.test(release);
|
||||||
|
|
||||||
// Reject non-numerical values with invalid uuid/hash lengths
|
// Reject non-numerical values with invalid uuid/hash lengths
|
||||||
@ -75,17 +75,19 @@ export async function disambiguateReleaseParam(
|
|||||||
return (await balena.models.release.get(release, { $select: 'id' })).id;
|
return (await balena.models.release.get(release, { $select: 'id' })).id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/require-await -- oclif parse functions require a Promise return */
|
||||||
/**
|
/**
|
||||||
* Convert to lowercase if looks like slug
|
* Convert to lowercase if looks like slug
|
||||||
*/
|
*/
|
||||||
export async function lowercaseIfSlug(s: string) {
|
export async function lowercaseIfSlug(s: string) {
|
||||||
|
/* eslint-enable @typescript-eslint/require-await */
|
||||||
return s.includes('/') ? s.toLowerCase() : s;
|
return s.includes('/') ? s.toLowerCase() : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeOsVersion(version: string) {
|
export function normalizeOsVersion(version: string) {
|
||||||
// Note that `version` may also be 'latest', 'recommended', 'default'
|
// Note that `version` may also be 'latest', 'recommended', 'default'
|
||||||
if (/^v?\d+\.\d+\.\d+/.test(version)) {
|
if (/^v?\d+\.\d+\.\d+/.test(version)) {
|
||||||
if (version[0] === 'v') {
|
if (version.startsWith('v')) {
|
||||||
version = version.slice(1);
|
version = version.slice(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ export function capitanoizeOclifUsage(
|
|||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCommandsFromManifest() {
|
export function getCommandsFromManifest() {
|
||||||
const manifest = require('../../oclif.manifest.json');
|
const manifest = require('../../oclif.manifest.json');
|
||||||
|
|
||||||
if (manifest.commands == null) {
|
if (manifest.commands == null) {
|
||||||
|
@ -119,7 +119,7 @@ export const checkLoggedInIf = async (doCheck: boolean) => {
|
|||||||
*
|
*
|
||||||
* @throws {NotAvailableInOfflineModeError}
|
* @throws {NotAvailableInOfflineModeError}
|
||||||
*/
|
*/
|
||||||
export const checkNotUsingOfflineMode = async () => {
|
export const checkNotUsingOfflineMode = () => {
|
||||||
if (process.env.BALENARC_OFFLINE_MODE) {
|
if (process.env.BALENARC_OFFLINE_MODE) {
|
||||||
throw new NotAvailableInOfflineModeError(stripIndent`
|
throw new NotAvailableInOfflineModeError(stripIndent`
|
||||||
This command requires an internet connection, and cannot be used in offline mode.
|
This command requires an internet connection, and cannot be used in offline mode.
|
||||||
|
@ -390,13 +390,12 @@ async function createApplication(
|
|||||||
try {
|
try {
|
||||||
const userInfo = await sdk.auth.getUserInfo();
|
const userInfo = await sdk.auth.getUserInfo();
|
||||||
username = userInfo.username;
|
username = userInfo.username;
|
||||||
} catch (err) {
|
} catch {
|
||||||
throw new sdk.errors.BalenaNotLoggedIn();
|
throw new sdk.errors.BalenaNotLoggedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-async-promise-executor
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
const applicationName = await new Promise<string>(async (resolve, reject) => {
|
const applicationName = await new Promise<string>(async (resolve, reject) => {
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const appName = await getCliForm().ask({
|
const appName = await getCliForm().ask({
|
||||||
@ -418,11 +417,13 @@ async function createApplication(
|
|||||||
'You already have a fleet with that name; please choose another.',
|
'You already have a fleet with that name; please choose another.',
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
} catch (err) {
|
} catch {
|
||||||
return resolve(appName);
|
resolve(appName);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return reject(err);
|
reject(err as Error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -452,9 +453,7 @@ async function generateApplicationConfig(
|
|||||||
const manifest = await sdk.models.config.getDeviceTypeManifestBySlug(
|
const manifest = await sdk.models.config.getDeviceTypeManifestBySlug(
|
||||||
app.is_for__device_type[0].slug,
|
app.is_for__device_type[0].slug,
|
||||||
);
|
);
|
||||||
const opts =
|
const opts = manifest.options?.filter((opt) => opt.name !== 'network');
|
||||||
manifest.options &&
|
|
||||||
manifest.options.filter((opt) => opt.name !== 'network');
|
|
||||||
|
|
||||||
const override = {
|
const override = {
|
||||||
appUpdatePollInterval: options.appUpdatePollInterval,
|
appUpdatePollInterval: options.appUpdatePollInterval,
|
||||||
|
@ -50,7 +50,7 @@ export function copyQemu(context: string, arch: string) {
|
|||||||
.then(() => getQemuPath(arch))
|
.then(() => getQemuPath(arch))
|
||||||
.then(
|
.then(
|
||||||
(qemu) =>
|
(qemu) =>
|
||||||
new Promise(function (resolve, reject) {
|
new Promise<void>(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 request = await import('request');
|
const { default: got } = await import('got');
|
||||||
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');
|
||||||
@ -114,10 +114,11 @@ async function installQemu(arch: string, qemuPath: string) {
|
|||||||
stream.resume();
|
stream.resume();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err as Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
request(qemuUrl)
|
got.stream
|
||||||
|
.get(qemuUrl)
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.pipe(zlib.createGunzip())
|
.pipe(zlib.createGunzip())
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
|
@ -16,7 +16,8 @@ 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 * as request from 'request';
|
import type { PlainResponse } from 'got';
|
||||||
|
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');
|
||||||
@ -110,7 +111,7 @@ export async function startRemoteBuild(
|
|||||||
const [buildRequest, stream] = await getRemoteBuildStream(build);
|
const [buildRequest, stream] = await getRemoteBuildStream(build);
|
||||||
|
|
||||||
// Setup CTRL-C handler so the user can interrupt the build
|
// Setup CTRL-C handler so the user can interrupt the build
|
||||||
let cancellationPromise = Promise.resolve();
|
let cancellationPromise: Promise<void> | undefined;
|
||||||
const sigintHandler = () => {
|
const sigintHandler = () => {
|
||||||
process.exitCode = 130;
|
process.exitCode = 130;
|
||||||
console.error('\nReceived SIGINT, cleaning up. Please wait.');
|
console.error('\nReceived SIGINT, cleaning up. Please wait.');
|
||||||
@ -119,7 +120,7 @@ export async function startRemoteBuild(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err.message);
|
console.error(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
buildRequest.abort();
|
buildRequest.destroy();
|
||||||
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);
|
||||||
@ -246,7 +247,8 @@ function getBuilderMessageHandler(
|
|||||||
console.error(`[debug] handling message: ${JSON.stringify(obj)}`);
|
console.error(`[debug] handling message: ${JSON.stringify(obj)}`);
|
||||||
}
|
}
|
||||||
if (obj.type != null && obj.type === 'metadata') {
|
if (obj.type != null && obj.type === 'metadata') {
|
||||||
return handleBuilderMetadata(obj, build);
|
handleBuilderMetadata(obj, build);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (obj.message) {
|
if (obj.message) {
|
||||||
readline.clearLine(process.stdout, 0);
|
readline.clearLine(process.stdout, 0);
|
||||||
@ -336,32 +338,29 @@ 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
|
|
||||||
*/
|
*/
|
||||||
function createRemoteBuildRequest(
|
async 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 zlib = require('zlib') as typeof import('zlib');
|
const { default: got } = await import('got');
|
||||||
|
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 request
|
return got.stream
|
||||||
.post({
|
.post(builderUrl, {
|
||||||
url: builderUrl,
|
headers: {
|
||||||
auth: { bearer: build.auth },
|
Authorization: `Bearer ${build.auth}`,
|
||||||
headers: { 'Content-Encoding': 'gzip' },
|
'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: request.RequestResponse) => {
|
.once('response', (response: PlainResponse) => {
|
||||||
if (response.statusCode >= 100 && response.statusCode < 400) {
|
if (response.statusCode >= 100 && response.statusCode < 400) {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
console.error(
|
console.error(
|
||||||
@ -373,8 +372,8 @@ function createRemoteBuildRequest(
|
|||||||
'Remote builder responded with HTTP error:',
|
'Remote builder responded with HTTP error:',
|
||||||
`${response.statusCode} ${response.statusMessage}`,
|
`${response.statusCode} ${response.statusMessage}`,
|
||||||
];
|
];
|
||||||
if (response.body) {
|
if (response.rawBody) {
|
||||||
msgArr.push(response.body);
|
msgArr.push(response.rawBody.toString());
|
||||||
}
|
}
|
||||||
onError(new ExpectedError(msgArr.join('\n')));
|
onError(new ExpectedError(msgArr.join('\n')));
|
||||||
}
|
}
|
||||||
@ -383,7 +382,7 @@ function createRemoteBuildRequest(
|
|||||||
|
|
||||||
async function getRemoteBuildStream(
|
async function getRemoteBuildStream(
|
||||||
build: RemoteBuild,
|
build: RemoteBuild,
|
||||||
): Promise<[request.Request, Stream.Stream]> {
|
): Promise<[ReturnType<typeof got.stream.post>, Stream.Stream]> {
|
||||||
const builderUrl = await getBuilderEndpoint(
|
const builderUrl = await getBuilderEndpoint(
|
||||||
build.baseUrl,
|
build.baseUrl,
|
||||||
build.appSlug,
|
build.appSlug,
|
||||||
@ -411,7 +410,7 @@ async function getRemoteBuildStream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tarStream = await getTarStream(build);
|
const tarStream = await getTarStream(build);
|
||||||
const buildRequest = createRemoteBuildRequest(
|
const buildRequest = await createRemoteBuildRequest(
|
||||||
build,
|
build,
|
||||||
tarStream,
|
tarStream,
|
||||||
builderUrl,
|
builderUrl,
|
||||||
@ -423,10 +422,20 @@ async function getRemoteBuildStream(
|
|||||||
stream = buildRequest.pipe(JSONStream.parse('*')) as NodeJS.ReadStream;
|
stream = buildRequest.pipe(JSONStream.parse('*')) as NodeJS.ReadStream;
|
||||||
}
|
}
|
||||||
stream = stream
|
stream = stream
|
||||||
.once('error', () => uploadSpinner.stop())
|
.once('error', () => {
|
||||||
.once('close', () => uploadSpinner.stop())
|
uploadSpinner.stop();
|
||||||
.once('data', () => uploadSpinner.stop())
|
})
|
||||||
.once('end', () => uploadSpinner.stop())
|
.once('close', () => {
|
||||||
.once('finish', () => uploadSpinner.stop());
|
uploadSpinner.stop();
|
||||||
|
})
|
||||||
|
.once('data', () => {
|
||||||
|
uploadSpinner.stop();
|
||||||
|
})
|
||||||
|
.once('end', () => {
|
||||||
|
uploadSpinner.stop();
|
||||||
|
})
|
||||||
|
.once('finish', () => {
|
||||||
|
uploadSpinner.stop();
|
||||||
|
});
|
||||||
return [buildRequest, stream];
|
return [buildRequest, stream];
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ export function sshArgsForRemoteCommand({
|
|||||||
...['-o', 'LogLevel=ERROR'],
|
...['-o', 'LogLevel=ERROR'],
|
||||||
...['-o', 'StrictHostKeyChecking=no'],
|
...['-o', 'StrictHostKeyChecking=no'],
|
||||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||||
...(proxyCommand && proxyCommand.length
|
...(proxyCommand?.length
|
||||||
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
|
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
|
||||||
: []),
|
: []),
|
||||||
`${username}@${hostname}`,
|
`${username}@${hostname}`,
|
||||||
@ -155,9 +155,9 @@ export async function runRemoteCommand({
|
|||||||
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
|
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
|
||||||
const ps = spawn(program, args, { stdio })
|
const ps = spawn(program, args, { stdio })
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.on('close', (code, signal) =>
|
.on('close', (code, signal) => {
|
||||||
resolve([code ?? undefined, signal ?? undefined]),
|
resolve([code ?? undefined, signal ?? undefined]);
|
||||||
);
|
});
|
||||||
|
|
||||||
if (ps.stdin && stdin && typeof stdin !== 'string') {
|
if (ps.stdin && stdin && typeof stdin !== 'string') {
|
||||||
stdin.pipe(ps.stdin);
|
stdin.pipe(ps.stdin);
|
||||||
@ -272,7 +272,7 @@ export async function getLocalDeviceCmdStdout(
|
|||||||
export const isRootUserGood = _.memoize(async (hostname: string, port) => {
|
export const isRootUserGood = _.memoize(async (hostname: string, port) => {
|
||||||
try {
|
try {
|
||||||
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...stdioIgnore });
|
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...stdioIgnore });
|
||||||
} catch (e) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -29,7 +29,11 @@ export function buffer(
|
|||||||
new Promise(function (resolve, reject) {
|
new Promise(function (resolve, reject) {
|
||||||
const fstream = fs.createReadStream(bufferFile);
|
const fstream = fs.createReadStream(bufferFile);
|
||||||
|
|
||||||
fstream.on('open', () => resolve(fstream)).on('error', reject);
|
fstream
|
||||||
|
.on('open', () => {
|
||||||
|
resolve(fstream);
|
||||||
|
})
|
||||||
|
.on('error', reject);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ async function spawnAndPipe(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function windosuExec(
|
function windosuExec(
|
||||||
escapedArgs: string[],
|
escapedArgs: string[],
|
||||||
stderr?: NodeJS.WritableStream,
|
stderr?: NodeJS.WritableStream,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -42,9 +42,9 @@ export = (stream: NodeJS.WriteStream = process.stdout) => {
|
|||||||
|
|
||||||
const showCursor = () => stream.write('\u001B[?25h');
|
const showCursor = () => stream.write('\u001B[?25h');
|
||||||
|
|
||||||
const cursorUp = (rows: number = 0) => stream.write(`\u001B[${rows}A`);
|
const cursorUp = (rows = 0) => stream.write(`\u001B[${rows}A`);
|
||||||
|
|
||||||
const cursorDown = (rows: number = 0) => stream.write(`\u001B[${rows}B`);
|
const cursorDown = (rows = 0) => stream.write(`\u001B[${rows}B`);
|
||||||
|
|
||||||
const write = (str: string) => stream.write(str);
|
const write = (str: string) => stream.write(str);
|
||||||
|
|
||||||
|
@ -61,11 +61,11 @@ export const tunnelConnectionToDevice = (
|
|||||||
client.pipe(remote);
|
client.pipe(remote);
|
||||||
remote.pipe(client);
|
remote.pipe(client);
|
||||||
remote.on('error', (err) => {
|
remote.on('error', (err) => {
|
||||||
console.error('Remote: ' + err);
|
console.error(`Remote: ${err}`);
|
||||||
client.end();
|
client.end();
|
||||||
});
|
});
|
||||||
client.on('error', (err) => {
|
client.on('error', (err) => {
|
||||||
console.error('Client: ' + err);
|
console.error(`Client: ${err}`);
|
||||||
remote.end();
|
remote.end();
|
||||||
});
|
});
|
||||||
remote.on('close', () => {
|
remote.on('close', () => {
|
||||||
|
@ -87,7 +87,8 @@ export function looksLikeInteger(input: string) {
|
|||||||
return /^(?:0|[1-9][0-9]*)$/.test(input);
|
return /^(?:0|[1-9][0-9]*)$/.test(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseAsInteger(input: string, paramName?: string) {
|
// eslint-disable-next-line @typescript-eslint/require-await -- oclif parse functions require a Promise return
|
||||||
|
export async function parseAsInteger(input: string, paramName?: string) {
|
||||||
if (!looksLikeInteger(input)) {
|
if (!looksLikeInteger(input)) {
|
||||||
const message =
|
const message =
|
||||||
paramName == null
|
paramName == null
|
||||||
@ -100,14 +101,15 @@ export function parseAsInteger(input: string, paramName?: string) {
|
|||||||
return Number(input);
|
return Number(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tryAsInteger(input: string): number | string {
|
export async function tryAsInteger(input: string): Promise<number | string> {
|
||||||
try {
|
try {
|
||||||
return parseAsInteger(input);
|
return await parseAsInteger(input);
|
||||||
} catch {
|
} catch {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-await -- oclif parse functions require a Promise return
|
||||||
export async function parseAsLocalHostnameOrIp(input: string) {
|
export async function parseAsLocalHostnameOrIp(input: string) {
|
||||||
if (input && !validateLocalHostnameOrIp(input)) {
|
if (input && !validateLocalHostnameOrIp(input)) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
|
@ -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 * as request from 'request';
|
import got from 'got';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
import { LoginServer } from '../../build/auth/server';
|
import { LoginServer } from '../../build/auth/server';
|
||||||
@ -31,7 +31,7 @@ chai.use(chaiAsPromised);
|
|||||||
|
|
||||||
const { expect } = chai;
|
const { expect } = chai;
|
||||||
|
|
||||||
async function getPage(name: string): Promise<string> {
|
function getPage(name: string): string {
|
||||||
const pagePath = path.join(
|
const pagePath = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'..',
|
||||||
@ -61,38 +61,30 @@ describe('Login server:', function () {
|
|||||||
server.shutdown();
|
server.shutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function testLogin(opt: {
|
async function testLogin({
|
||||||
|
verb = 'post',
|
||||||
|
...opt
|
||||||
|
}: {
|
||||||
expectedBody: string;
|
expectedBody: string;
|
||||||
expectedErrorMsg?: string;
|
expectedErrorMsg?: string;
|
||||||
expectedStatusCode: number;
|
expectedStatusCode: number;
|
||||||
expectedToken: string;
|
expectedToken: string;
|
||||||
urlPath?: string;
|
urlPath?: string;
|
||||||
verb?: string;
|
verb?: 'post' | 'put';
|
||||||
}) {
|
}) {
|
||||||
opt.urlPath = opt.urlPath ?? addr.urlPath;
|
opt.urlPath = opt.urlPath ?? addr.urlPath;
|
||||||
const post = opt.verb
|
const res = await got[verb](
|
||||||
? ((request as any)[opt.verb] as typeof request.post)
|
`http://${addr.host}:${addr.port}${opt.urlPath}`,
|
||||||
: request.post;
|
{
|
||||||
await new Promise<void>((resolve, reject) => {
|
form: {
|
||||||
post(
|
token: opt.expectedToken,
|
||||||
`http://${addr.host}:${addr.port}${opt.urlPath}`,
|
|
||||||
{
|
|
||||||
form: {
|
|
||||||
token: opt.expectedToken,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
function (error, response, body) {
|
throwHttpErrors: false,
|
||||||
try {
|
},
|
||||||
expect(error).to.not.exist;
|
);
|
||||||
expect(response.statusCode).to.equal(opt.expectedStatusCode);
|
|
||||||
expect(body).to.equal(opt.expectedBody);
|
expect(res.body).to.equal(opt.expectedBody);
|
||||||
resolve();
|
expect(res.statusCode).to.equal(opt.expectedStatusCode);
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = await server.awaitForToken();
|
const token = await server.awaitForToken();
|
||||||
@ -127,14 +119,14 @@ 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: 'get',
|
verb: 'put',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('given the token authenticates with the server', function () {
|
describe('given the token authenticates with the server', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
|
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
|
||||||
this.loginIfTokenValidStub.returns(Promise.resolve(true));
|
this.loginIfTokenValidStub.resolves(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
@ -143,7 +135,7 @@ describe('Login server:', function () {
|
|||||||
|
|
||||||
it('should eventually be the token', async () => {
|
it('should eventually be the token', async () => {
|
||||||
await testLogin({
|
await testLogin({
|
||||||
expectedBody: await getPage('success'),
|
expectedBody: getPage('success'),
|
||||||
expectedStatusCode: 200,
|
expectedStatusCode: 200,
|
||||||
expectedToken: tokens.johndoe.token,
|
expectedToken: tokens.johndoe.token,
|
||||||
});
|
});
|
||||||
@ -153,7 +145,7 @@ describe('Login server:', function () {
|
|||||||
describe('given the token does not authenticate with the server', function () {
|
describe('given the token does not authenticate with the server', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
|
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
|
||||||
return this.loginIfTokenValidStub.returns(Promise.resolve(false));
|
return this.loginIfTokenValidStub.resolves(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
@ -162,7 +154,7 @@ describe('Login server:', function () {
|
|||||||
|
|
||||||
it('should be rejected', async () => {
|
it('should be rejected', async () => {
|
||||||
await testLogin({
|
await testLogin({
|
||||||
expectedBody: await getPage('error'),
|
expectedBody: getPage('error'),
|
||||||
expectedStatusCode: 401,
|
expectedStatusCode: 401,
|
||||||
expectedToken: tokens.johndoe.token,
|
expectedToken: tokens.johndoe.token,
|
||||||
expectedErrorMsg: 'Invalid token',
|
expectedErrorMsg: 'Invalid token',
|
||||||
@ -171,7 +163,7 @@ describe('Login server:', function () {
|
|||||||
|
|
||||||
it('should be rejected if no token', async () => {
|
it('should be rejected if no token', async () => {
|
||||||
await testLogin({
|
await testLogin({
|
||||||
expectedBody: await getPage('error'),
|
expectedBody: getPage('error'),
|
||||||
expectedStatusCode: 401,
|
expectedStatusCode: 401,
|
||||||
expectedToken: '',
|
expectedToken: '',
|
||||||
expectedErrorMsg: 'No token',
|
expectedErrorMsg: 'No token',
|
||||||
@ -180,7 +172,7 @@ describe('Login server:', function () {
|
|||||||
|
|
||||||
it('should be rejected if token is malformed', async () => {
|
it('should be rejected if token is malformed', async () => {
|
||||||
await testLogin({
|
await testLogin({
|
||||||
expectedBody: await getPage('error'),
|
expectedBody: getPage('error'),
|
||||||
expectedStatusCode: 401,
|
expectedStatusCode: 401,
|
||||||
expectedToken: 'asdf',
|
expectedToken: 'asdf',
|
||||||
expectedErrorMsg: 'Invalid token',
|
expectedErrorMsg: 'Invalid token',
|
||||||
|
@ -267,15 +267,15 @@ describe('balena build', function () {
|
|||||||
...fsMod,
|
...fsMod,
|
||||||
promises: {
|
promises: {
|
||||||
...fsMod.promises,
|
...fsMod.promises,
|
||||||
access: async (p: string) =>
|
access: (p: string) =>
|
||||||
p === qemuBinPath ? undefined : fsMod.promises.access(p),
|
p === qemuBinPath ? undefined : fsMod.promises.access(p),
|
||||||
stat: async (p: string) =>
|
stat: (p: string) =>
|
||||||
p === qemuBinPath ? { size: 1 } : fsMod.promises.stat(p),
|
p === qemuBinPath ? { size: 1 } : fsMod.promises.stat(p),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mock(qemuModPath, {
|
mock(qemuModPath, {
|
||||||
...qemuMod,
|
...qemuMod,
|
||||||
copyQemu: async () => '',
|
copyQemu: () => '',
|
||||||
});
|
});
|
||||||
mock.reRequire('../../build/utils/qemu');
|
mock.reRequire('../../build/utils/qemu');
|
||||||
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });
|
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });
|
||||||
@ -442,6 +442,93 @@ 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, switchSentry } from '../helpers';
|
import { cleanOutput, runCommand } from '../helpers';
|
||||||
import type {
|
import type {
|
||||||
ExpectedTarStreamFiles,
|
ExpectedTarStreamFiles,
|
||||||
ExpectedTarStreamFilesByService,
|
ExpectedTarStreamFilesByService,
|
||||||
@ -262,7 +262,6 @@ 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' },
|
||||||
@ -319,7 +318,6 @@ 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({
|
||||||
@ -337,9 +335,8 @@ describe('balena deploy', function () {
|
|||||||
});
|
});
|
||||||
expect(failedImagePatchRequests).to.equal(maxRequestRetries);
|
expect(failedImagePatchRequests).to.equal(maxRequestRetries);
|
||||||
} finally {
|
} finally {
|
||||||
await switchSentry(sentryStatus);
|
// We mock process.exit and need to force cast it to a SinonStub to restore it
|
||||||
// @ts-expect-error claims restore does not exist
|
(process.exit as unknown as sinon.SinonStub).restore();
|
||||||
process.exit.restore();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -114,6 +114,14 @@ 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,8 +21,6 @@ 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;
|
||||||
@ -39,10 +37,7 @@ describe('balena device logs', function () {
|
|||||||
supervisor.done();
|
supervisor.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
// skip non-standalone tests because nock's mock socket causes the error:
|
it('should reach the expected endpoints on a local device', async () => {
|
||||||
// "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();
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user