mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
1 Commits
v16.2.0
...
output-fra
Author | SHA1 | Date | |
---|---|---|---|
ab1d8aa6ba |
133
.github/actions/publish/action.yml
vendored
133
.github/actions/publish/action.yml
vendored
@ -1,133 +0,0 @@
|
||||
---
|
||||
name: package and draft GitHub release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
variables:
|
||||
description: "JSON stringified object containing all the variables from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
XCODE_APP_LOADER_EMAIL:
|
||||
type: string
|
||||
default: "accounts+apple@balena.io"
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '16.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Extract custom source artifact
|
||||
shell: pwsh
|
||||
working-directory: .
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Install additional tools
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
choco install yq
|
||||
|
||||
- name: Install additional tools
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
brew install coreutils
|
||||
|
||||
# https://www.electron.build/code-signing.html
|
||||
# https://github.com/Apple-Actions/import-codesign-certs
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
|
||||
- name: Import Windows code signing certificate
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: |
|
||||
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
|
||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
||||
|
||||
Import-PfxCertificate `
|
||||
-FilePath ${{ runner.temp }}/certificate.pfx `
|
||||
-CertStoreLocation Cert:\CurrentUser\My `
|
||||
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
|
||||
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
|
||||
# https://github.com/product-os/scripts/tree/master/shared
|
||||
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
|
||||
- name: Package release
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if [[ $runner_os =~ darwin|macos|osx ]]; then
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
CSC_KEYCHAIN=signing_temp
|
||||
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
|
||||
elif [[ $runner_os =~ windows|win ]]; then
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
CSC_LINK='${{ runner.temp }}\certificate.pfx'
|
||||
|
||||
# patches/all/oclif.patch
|
||||
MSYSSHELLPATH="$(which bash)"
|
||||
MSYSTEM=MSYS
|
||||
|
||||
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
|
||||
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
|
||||
fi
|
||||
|
||||
npm run package
|
||||
|
||||
find dist -type f -maxdepth 1
|
||||
|
||||
env:
|
||||
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
|
||||
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
# https://sectigo.com/resource-library/time-stamping-server
|
||||
TIMESTAMP_SERVER: http://timestamp.sectigo.com
|
||||
# Apple notarization (automation/build-bin.ts)
|
||||
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
|
||||
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
|
||||
path: dist
|
||||
retention-days: 1
|
59
.github/actions/test/action.yml
vendored
59
.github/actions/test/action.yml
vendored
@ -1,59 +0,0 @@
|
||||
---
|
||||
name: test release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
variables:
|
||||
description: "JSON stringified object containing all the variables from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '16.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Test release
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
if [[ -e package-lock.json ]] || [[ -e npm-shrinkwrap.json ]]; then
|
||||
npm ci
|
||||
else
|
||||
npm i
|
||||
fi
|
||||
|
||||
npm run build
|
||||
npm run test
|
||||
|
||||
- name: Compress custom source
|
||||
shell: pwsh
|
||||
run: tar -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
retention-days: 1
|
29
.github/workflows/flowzone.yml
vendored
29
.github/workflows/flowzone.yml
vendored
@ -1,29 +0,0 @@
|
||||
name: Flowzone
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
flowzone:
|
||||
name: Flowzone
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@v4.7.1
|
||||
# prevent duplicate workflow executions for pull_request and pull_request_target
|
||||
if: |
|
||||
(
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.event_name == 'pull_request'
|
||||
) || (
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
github.event_name == 'pull_request_target'
|
||||
)
|
||||
secrets: inherit
|
||||
with:
|
||||
custom_runs_on: '[["self-hosted","Linux","distro:focal","X64"],["self-hosted","Linux","distro:focal","ARM64"],["macos-12"],["windows-2019"]]'
|
||||
repo_config: true
|
||||
repo_description: "The official balena CLI tool."
|
||||
github_prerelease: true
|
15
.resinci.yml
Normal file
15
.resinci.yml
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
npm:
|
||||
platforms:
|
||||
- name: linux
|
||||
os: ubuntu
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "12"
|
||||
- "14"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "12"
|
||||
- "14"
|
File diff suppressed because it is too large
Load Diff
927
CHANGELOG.md
927
CHANGELOG.md
@ -4,933 +4,6 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 16.2.0 - 2023-05-19
|
||||
|
||||
* secureboot: Retrieve the OS release & contract in one request [Thodoris Greasidis]
|
||||
* os configure, config generate: Add '--secureBoot' option to opt-in secure boot [Alex Gonzalez]
|
||||
|
||||
<details>
|
||||
<summary> package.json: Update balena-sdk to 16.44.2 [Alex Gonzalez] </summary>
|
||||
|
||||
> ### balena-sdk-16.44.2 - 2023-05-16
|
||||
>
|
||||
> * Update flowzone's macos runner to v12 [Thodoris Greasidis]
|
||||
> * Add device type yocto properties to typings [Otávio Jacobi]
|
||||
> * Optimize getDeviceUrl request in one query [Otávio Jacobi]
|
||||
>
|
||||
> ### balena-sdk-16.44.1 - 2023-05-09
|
||||
>
|
||||
> * Fix device.getAllByOrganization parameter docs [Otávio Jacobi]
|
||||
>
|
||||
> ### balena-sdk-16.44.0 - 2023-04-27
|
||||
>
|
||||
> * Add device.getAllByOrganization() [Thodoris Greasidis]
|
||||
> * Deprecate Device's is_managed_by__device & manages__device properties [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.43.0 - 2023-04-19
|
||||
>
|
||||
> * Add test case DeviceHistory expandable resources [fisehara]
|
||||
> * Make DeviceHistory referenced resources expandable [fisehara]
|
||||
>
|
||||
> ### balena-sdk-16.42.0 - 2023-04-18
|
||||
>
|
||||
> * Add support for pine queries on Concept Type properties [Thodoris Greasidis]
|
||||
> * Properly type Actor properties on resources [fisehara]
|
||||
>
|
||||
> ### balena-sdk-16.41.0 - 2023-04-07
|
||||
>
|
||||
> * Release model: Add support for getting/patching releases by `application` & `rawVersion` pairs [myarmolinsky]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
* flowzone: update custom runs to use macos-12 [Alex Gonzalez]
|
||||
|
||||
## 16.1.0 - 2023-05-16
|
||||
|
||||
* build linux/arm packages [balenaCI]
|
||||
|
||||
## 16.0.0 - 2023-05-16
|
||||
|
||||
* support: Change the printed support expiry date in ISO 8601 UTC format [Thodoris Greasidis]
|
||||
* logs: Change the timestamp format to ISO 8601 UTC [Thodoris Greasidis]
|
||||
* Pin flowzone to v4.7.1 [Felipe Lalanne]
|
||||
* Update etcher-sdk to v8.5.3 [Felipe Lalanne]
|
||||
* Update vercel/pkg to v5.8.1 [Felipe Lalanne]
|
||||
* Update to Node 16 [Felipe Lalanne]
|
||||
|
||||
## 15.2.3 - 2023-05-03
|
||||
|
||||
* Use valid release uuid for local releases [Felipe Lalanne]
|
||||
|
||||
## 15.2.2 - 2023-04-28
|
||||
|
||||
* Remove nvmrc [Felipe Lalanne]
|
||||
|
||||
## 15.2.1 - 2023-04-28
|
||||
|
||||
* Fix tslib going out of sync causing HUP to fail [Thodoris Greasidis]
|
||||
|
||||
## 15.2.0 - 2023-04-05
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Add support for device restarts in open-balena [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-sdk-16.40.0 - 2023-04-05
|
||||
>
|
||||
> * device.reboot: Fix the typings requiring a second argument [Thodoris Greasidis]
|
||||
> * device.restartApplication: Use the supervisor endpoint to issue restarts [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.39.1 - 2023-04-04
|
||||
>
|
||||
> * patch: Split instruction strings on linebreak [Vipul Gupta (@vipulgupta2048)]
|
||||
>
|
||||
> ### balena-sdk-16.39.0 - Invalid date
|
||||
>
|
||||
> * Add `device history` model [fisehara]
|
||||
>
|
||||
> ### balena-sdk-16.38.2 - 2023-03-28
|
||||
>
|
||||
> * Fix credit-bundle jsdocs [Josh Bowling]
|
||||
>
|
||||
> ### balena-sdk-16.38.1 - 2023-03-27
|
||||
>
|
||||
> * Deprecate the device-type.json's instructions field [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.38.0 - 2023-03-21
|
||||
>
|
||||
> * Add aliases for the DT contrast slugs used in getInstructions [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.37.0 - 2023-03-21
|
||||
>
|
||||
> * device-type/getInstructions: Overload to accept the device type contract [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.36.6 - 2023-03-20
|
||||
>
|
||||
> * Update TypeScript to 5.0.2 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.36.5 - 2023-03-16
|
||||
>
|
||||
> * patch: Improve jetsonFlash provisioning partial [Vipul Gupta (@vipulgupta2048)]
|
||||
>
|
||||
> ### balena-sdk-16.36.4 - 2023-03-15
|
||||
>
|
||||
> * Avoid running write operation tests in parallel to support retries [Thodoris Greasidis]
|
||||
> * Retry failing tests twice [Thodoris Greasidis]
|
||||
> * Fix tests per removal of `microservices-starter` application type [myarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.36.3 - 2023-02-28
|
||||
>
|
||||
> * models/device-type: Add test for Radxa Zero instructions [Alexandru Costache]
|
||||
> * lib/models: Add radxaFlash protocol for Radxa boards [Alexandru Costache]
|
||||
>
|
||||
> ### balena-sdk-16.36.2 - 2023-02-24
|
||||
>
|
||||
> * tests: Stop using flowzone internal env vars to for skipping npm test [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.36.1 - 2023-02-20
|
||||
>
|
||||
> * Add plan validity date fields [Josh Bowling]
|
||||
>
|
||||
> ### balena-sdk-16.36.0 - 2023-02-16
|
||||
>
|
||||
> * Add contract partial based instruction generation [Micah Halter]
|
||||
>
|
||||
> ### balena-sdk-16.35.0 - 2023-02-10
|
||||
>
|
||||
> * Add `CreditBundle` model [myarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.34.0 - 2023-02-09
|
||||
>
|
||||
> * Add configVarInvalidRegex to Config Var typing [Felipe Lalanne]
|
||||
>
|
||||
> ### balena-sdk-16.33.0 - 2023-02-09
|
||||
>
|
||||
> * CurrentServiceWithCommit: Add release `raw_version` to type [myarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.32.3 - 2023-02-07
|
||||
>
|
||||
> * Optimize the device.get method [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.32.2 - 2023-02-02
|
||||
>
|
||||
> * Improve pine typings for public resources without id fields [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.32.1 - 2023-01-16
|
||||
>
|
||||
> * Drop no longer used .travis.yml & .hound.yml [Thodoris Greasidis]
|
||||
> * Rerun prettier [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.32.0 - 2023-01-05
|
||||
>
|
||||
> * typings: Add the device.is_frozen field [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.31.2 - 2022-12-20
|
||||
>
|
||||
> * application.create: Deprecate the `parent` option [Thodoris Greasidis]
|
||||
> * Deprecate the device.getAllByParentDevice() method [Thodoris Greasidis]
|
||||
> * Simplify the device.move() checks [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.31.1 - 2022-12-17
|
||||
>
|
||||
> * Replace appveyor with flowzone [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.31.0 - 2022-12-16
|
||||
>
|
||||
> * Add `updateAccountInfo` method to billing model for updating billing account info [myarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.30.2 - 2022-12-13
|
||||
>
|
||||
> * Flowzone: Allow external contributions [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.30.1 - 2022-12-07
|
||||
>
|
||||
> * patch: bump catch-uncommitted from 1.6.2 to 2.0.0 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.30.0 - 2022-11-24
|
||||
>
|
||||
> * Add utils and export mergePineOptions `balena.utils.mergePineOptions()` [JSReds]
|
||||
>
|
||||
> ### balena-sdk-16.29.3 - 2022-11-24
|
||||
>
|
||||
> * device.getWithServiceDetails: Stop auto-expanding the gateway_downloads [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.29.2 - 2022-11-16
|
||||
>
|
||||
> * Update TypeScript to 4.9.3 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.29.1 - 2022-11-12
|
||||
>
|
||||
> * Fix release end_timestamp type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.29.0 - 2022-11-12
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Support filtered $count operations inside $filter & $orderby [Thodoris Greasidis] </summary>
|
||||
>
|
||||
>> #### pinejs-client-js-6.12.0 - 2022-11-10
|
||||
>>
|
||||
>> * Deprecate the 'a/count' notation in $orderby [Thodoris Greasidis]
|
||||
>> * Deprecate the $count: { $op: number } notation [Thodoris Greasidis]
|
||||
>> * Add support for `$filter: { $op: [{ $count: {} }, number] }` notation [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### pinejs-client-js-6.11.0 - 2022-11-09
|
||||
>>
|
||||
>> * Deprecate non-$filter props inside `$expand: { a: { $count: {...}}}` [Thodoris Greasidis]
|
||||
>> * Add support for `$orderby: { a: { $count: ... }, $dir: 'asc' }` notation [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### pinejs-client-js-6.10.7 - 2022-11-07
|
||||
>>
|
||||
>> * Refactor the deprecation message definitions [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### pinejs-client-js-6.10.6 - 2022-11-01
|
||||
>>
|
||||
>> * tests: Support `.only` & `.skip` in the higher level test functions [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### pinejs-client-js-6.10.5 - 2022-10-14
|
||||
>>
|
||||
>> * Flowzone: Use inherited secrets [Pagan Gazzard]
|
||||
>>
|
||||
>> #### pinejs-client-js-6.10.4 - 2022-09-26
|
||||
>>
|
||||
>> * Specify node 10 as the minimum supported node engine in the package.json [Thodoris Greasidis]
|
||||
>> * Replace balenaCI with flowzone [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### pinejs-client-js-6.10.3 - 2022-09-15
|
||||
>>
|
||||
>> * Fix $count typings to only allow $filter under it [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### pinejs-client-js-6.10.2 - 2022-04-08
|
||||
>>
|
||||
>> * Update dependencies [Pagan Gazzard]
|
||||
>> * Remove circleci [Pagan Gazzard]
|
||||
>>
|
||||
>> #### pinejs-client-js-6.10.1 - 2022-02-08
|
||||
>>
|
||||
>> * Do not await the _request() result to allow enhanced promises downstream [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### pinejs-client-js-6.10.0 - 2022-01-24
|
||||
>>
|
||||
>> * Add optional retry logic to client [Paul Jonathan Zoulin]
|
||||
>>
|
||||
>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-sdk-16.28.4 - 2022-11-04
|
||||
>
|
||||
> * Use deep imports for date-fns to improve tree-shaking [Thodoris Greasidis]
|
||||
> * Enable esModuleInterop build option [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.28.3 - 2022-11-03
|
||||
>
|
||||
> * Update balena-errors to v4.7.3 [JSReds]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
## 15.1.3 - 2023-04-05
|
||||
|
||||
|
||||
<details>
|
||||
<summary> devices supported: Fix showing types without a valid & finalized release [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-sdk-16.28.2 - 2022-10-27
|
||||
>
|
||||
> * Update tests to run on node 18 [Thodoris Greasidis]
|
||||
> * deviceType.getAllSupported: Require a valid & final release to exist [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.28.1 - 2022-10-14
|
||||
>
|
||||
> * flowzone: Run the node tests using the latest LTS version [Thodoris Greasidis]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
## 15.1.2 - 2023-03-27
|
||||
|
||||
* Improve type checking by using the satisfies operator [Thodoris Greasidis]
|
||||
|
||||
## 15.1.1 - 2023-03-17
|
||||
|
||||
* Update TypeScript to 5.0.2 [Thodoris Greasidis]
|
||||
|
||||
## 15.1.0 - 2023-03-14
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-compose to v2.2.1 [Kyle Harding] </summary>
|
||||
|
||||
> ### balena-compose-2.2.1 - 2023-03-14
|
||||
>
|
||||
> * Ignore references to build stages when evaluating manifests [Kyle Harding]
|
||||
>
|
||||
> ### balena-compose-2.2.0 - 2023-03-13
|
||||
>
|
||||
> * OCI Image Index should allow platform opts [Kyle Harding]
|
||||
>
|
||||
> ### balena-compose-2.1.4 - 2023-03-13
|
||||
>
|
||||
> * Write to debug log when using platform option [Kyle Harding]
|
||||
>
|
||||
> ### balena-compose-2.1.3 - 2023-03-01
|
||||
>
|
||||
> * Fixup tests to use recent debian:bullseye-slim images [Kyle Harding]
|
||||
>
|
||||
> ### balena-compose-2.1.2 - 2022-10-17
|
||||
>
|
||||
> * test/multibuild: Use 127.0.0.1 for the extra_hosts test [Ken Bannister]
|
||||
> * Output error text to aid test debugging [Ken Bannister]
|
||||
> * Replace balenaCI & circleCI with flowzone [Thodoris Greasidis]
|
||||
> * Pin dockerode to v3.3.3 to avoid regression [Ken Bannister]
|
||||
> * Prettify fixup [Ken Bannister]
|
||||
> * Fix underspecified generics in release/models [Ken Bannister]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
## 15.0.6 - 2023-03-13
|
||||
|
||||
* Devices: explicitly fetches only used fields [Otávio Jacobi]
|
||||
|
||||
## 15.0.5 - 2023-03-10
|
||||
|
||||
* Fix application isLegacy check for rename and deploy [JSReds]
|
||||
|
||||
## 15.0.4 - 2023-02-21
|
||||
|
||||
* patch: Clarify update rate of update notifier info [Heath Raftery]
|
||||
|
||||
## 15.0.3 - 2023-01-18
|
||||
|
||||
* Use https for the npm deprecation check, avoiding a redirect [Pagan Gazzard]
|
||||
|
||||
## 15.0.2 - 2023-01-14
|
||||
|
||||
* Fix push --nolive doc typo [Josh Bowling]
|
||||
|
||||
## 15.0.1 - 2023-01-10
|
||||
|
||||
* Process livepush build logs inline [Felipe Lalanne]
|
||||
|
||||
## 15.0.0 - 2023-01-02
|
||||
|
||||
* preload: Drops ability to preload Intel Edison (EOL 2017) Upgrade balena-preload from 12.2.0 to 13.0.0 [JOASSART Edwin]
|
||||
|
||||
## 14.5.18 - 2022-12-29
|
||||
|
||||
* Update flowzone tests to use npm ci [Thodoris Greasidis]
|
||||
|
||||
## 14.5.17 - 2022-12-28
|
||||
|
||||
* Stop using the deprecated balena-sync module [Thodoris Greasidis]
|
||||
|
||||
## 14.5.16 - 2022-12-28
|
||||
|
||||
* Update the npm-shrinkwrap.json dependencies to match the package.json [Thodoris Greasidis]
|
||||
|
||||
## 14.5.15 - 2022-12-12
|
||||
|
||||
* patch: update balena-preload to 12.2.0 [Edwin Joassart]
|
||||
|
||||
## 14.5.14 - 2022-12-11
|
||||
|
||||
* Bump multicast-dns to rebased commit (again) [pipex]
|
||||
|
||||
## 14.5.13 - 2022-12-08
|
||||
|
||||
* Build on macos-11 for library compatibility reasons [Page-]
|
||||
* Build on ubuntu-20.04 for library compatibility reasons [Page-]
|
||||
|
||||
## 14.5.12 - 2022-11-21
|
||||
|
||||
* Move GH publishing to FZ core [ab77]
|
||||
|
||||
## 14.5.11 - 2022-11-17
|
||||
|
||||
* Adding .nvmrc so we can use nvm use instead of hunting for version [zoobot]
|
||||
|
||||
## 14.5.10 - 2022-11-11
|
||||
|
||||
* Fix surfacing incompatible device type errors as not recognized [Thodoris Greasidis]
|
||||
|
||||
## 14.5.9 - 2022-11-11
|
||||
|
||||
* Prevent git from existing with 141 [ab77]
|
||||
|
||||
## 14.5.8 - 2022-11-10
|
||||
|
||||
* Replace missing input [ab77]
|
||||
|
||||
## 14.5.7 - 2022-11-10
|
||||
|
||||
* Just ignore errors during publish [ab77]
|
||||
|
||||
## 14.5.6 - 2022-11-10
|
||||
|
||||
* Ignore PIPE signal [ab77]
|
||||
|
||||
## 14.5.5 - 2022-11-10
|
||||
|
||||
* Don't pipefail [ab77]
|
||||
|
||||
## 14.5.4 - 2022-11-10
|
||||
|
||||
* Error when the device type and image parameters do not match [Thodoris Greasidis]
|
||||
|
||||
## 14.5.3 - 2022-11-10
|
||||
|
||||
* Switch to Flowzone [ab77]
|
||||
|
||||
## 14.5.2 - 2022-10-21
|
||||
|
||||
* Stop waiting for the analytics response [Thodoris Greasidis]
|
||||
|
||||
## 14.5.1 - 2022-10-20
|
||||
|
||||
* Bump parse-link-header from 1.0.1 to 2.0.0 [dependabot[bot]]
|
||||
|
||||
## 14.5.0 - 2022-10-18
|
||||
|
||||
* keeps events loggiging with default message [Otávio Jacobi]
|
||||
* uses amplitude data events format [Otávio Jacobi]
|
||||
* changes analytics endpoint to analytics-backend [Otávio Jacobi]
|
||||
|
||||
## 14.4.4 - 2022-10-18
|
||||
|
||||
* Update simple-git to 3.14.1 [Thodoris Greasidis]
|
||||
|
||||
## 14.4.3 - 2022-10-17
|
||||
|
||||
* config generate: Fix the incompatible arch errors showing as not found [Thodoris Greasidis]
|
||||
|
||||
## 14.4.2 - 2022-10-17
|
||||
|
||||
* Stop relying on device-type.json for resolving the device type aliases [Thodoris Greasidis]
|
||||
* Stop relying on device-type.json for resolving the cpu architecture [Thodoris Greasidis]
|
||||
|
||||
## 14.4.1 - 2022-10-12
|
||||
|
||||
* balena os initialize: Clarify that the process includes flashing [Heath Raftery]
|
||||
|
||||
## 14.4.0 - 2022-10-12
|
||||
|
||||
* device register: Add support for the `--deviceType` option [Thodoris Greasidis]
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to 16.28.0 [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-sdk-16.28.0 - 2022-10-12
|
||||
>
|
||||
> * device.register: Allow providing a device type for the registered device [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.27.0 - 2022-10-07
|
||||
>
|
||||
> * Add support for batch operations for more device modifying methods [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.26.7 - 2022-10-07
|
||||
>
|
||||
> * Fix request batching chunking when there is no grouping navigation prop [Thodoris Greasidis]
|
||||
> * request-batching: Increase the batch size to 200 items [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.26.6 - 2022-10-06
|
||||
>
|
||||
> * Fix request batching not chunking the items of the operation [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.26.5 - 2022-09-26
|
||||
>
|
||||
> * Delete redundant .resinci.yml [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sdk-16.26.4 - 2022-09-23
|
||||
>
|
||||
> * Remove moment in favor of date-fns [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.26.3 - 2022-09-21
|
||||
>
|
||||
> * Skip running tests in flowzone till we can inject env vars [Thodoris Greasidis]
|
||||
> * Switch from balenaCI to flowzone [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sdk-16.26.2 - 2022-09-06
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update balena-register-device to 8.0.0 [Thodoris Greasidis] </summary>
|
||||
>
|
||||
>> #### balena-register-device-8.0.0 - 2022-09-06
|
||||
>>
|
||||
>> * Remove the travis & appveyor configurations [Thodoris Greasidis]
|
||||
>> * tsconfig: Enable strict type checking [Thodoris Greasidis]
|
||||
>> * Update devDependencies [Thodoris Greasidis]
|
||||
>> * Update the uuid package to v9 [Thodoris Greasidis]
|
||||
>> * Prevent creating a package-lock.json [Thodoris Greasidis]
|
||||
>> * Drop support for node 10 in favor of 14 & 16 [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-register-device-7.2.0 - 2021-04-29
|
||||
>>
|
||||
>> * Support `supervisorVersion`/`osVersion`/`osVariant`/`macAddress` fields [Pagan Gazzard]
|
||||
>>
|
||||
>> #### balena-register-device-7.1.1 - 2021-04-29
|
||||
>>
|
||||
>> * Update dependencies [Pagan Gazzard]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-sdk-16.26.1 - 2022-08-29
|
||||
>
|
||||
> * Update TypeScript to v4.8 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.26.0 - 2022-08-26
|
||||
>
|
||||
> * Pin TypeScript to 4.7 until upstream dependencies are updated [Thodoris Greasidis]
|
||||
> * types: Add the InvitationTokenDecodedPayload type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.25.1 - 2022-08-05
|
||||
>
|
||||
> * Deprecate the public_key from the user JWT [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.25.0 - 2022-08-04
|
||||
>
|
||||
> * application.remove: Support batch deletions by providing multiple IDs [Thodoris Greasidis]
|
||||
> * Refactor the request batching implementation to be generic [Thodoris Greasidis]
|
||||
> * Change pine options merging to extend the default `$select`ed properties [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.24.2 - 2022-08-02
|
||||
>
|
||||
> * Refactor the internal mergePineOptions utility [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.24.1 - 2022-07-21
|
||||
>
|
||||
> * Update Husky to v7 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.24.0 - 2022-07-08
|
||||
>
|
||||
> * types: Add missing Application to Service relation [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.23.0 - 2022-07-07
|
||||
>
|
||||
> * Add expiry-date for generation of user and device keys [Nitish Agarwal]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 14.3.1 - 2022-09-06
|
||||
|
||||
* Add unified OS versions in the examples of the device & os commands [Thodoris Greasidis]
|
||||
|
||||
## 14.3.0 - 2022-08-17
|
||||
|
||||
* release: Add `validate` command for validating releases [Matthew Yarmolinsky]
|
||||
* release: Add `invalidate` command for invalidating releases [Matthew Yarmolinsky]
|
||||
|
||||
## 14.2.0 - 2022-08-16
|
||||
|
||||
* fleet: Add `track-latest` command for tracking the latest release [Matthew Yarmolinsky]
|
||||
* fleet: Add `pin` command for pinning fleets to a specific release [Matthew Yarmolinsky]
|
||||
|
||||
## 14.1.0 - 2022-08-03
|
||||
|
||||
* Add device track command for pinning a device to the latest release or a specific release [Matthew Yarmolinsky]
|
||||
|
||||
## 14.0.0 - 2022-08-01
|
||||
|
||||
* Drop undocumented support for numeric ids in balena device commands [Matthew Yarmolinsky]
|
||||
* Drop support for the deprecated `balena device public-url <enable|disable|status> <uuid>` and related format [Matthew Yarmolinsky]
|
||||
* Drop support for numeric fleet id parameters from all commands [Matthew Yarmolinsky]
|
||||
* fleet: Add `--filter`, `--no-header`, `--no-truncate`, and `--sort` options [Matthew Yarmolinsky]
|
||||
* fleet: Add `--fields` and `--json` options [Matthew Yarmolinsky]
|
||||
* fleet: Use the oclif output formatter [Matthew Yarmolinsky]
|
||||
* config: Drop optional and ignored `--type` flag [Matthew Yarmolinsky]
|
||||
* Drop deprecated `--logs` flag [Matthew Yarmolinsky]
|
||||
* Drop support for open-balena-api < v0.131.0 [Matthew Yarmolinsky]
|
||||
|
||||
## 13.10.1 - 2022-08-01
|
||||
|
||||
* Fix balena deploy missing dependency error [Thodoris Greasidis]
|
||||
|
||||
## 13.10.0 - 2022-07-20
|
||||
|
||||
* Add `--view` flag to `device` command for opening a device's dashboard page [Matthew Yarmolinsky]
|
||||
|
||||
## 13.9.0 - 2022-07-19
|
||||
|
||||
* Switch to balena-compose [Akis Kesoglou]
|
||||
|
||||
## 13.8.0 - 2022-07-18
|
||||
|
||||
* Add `--note` option for `push` and `deploy` [Matthew Yarmolinsky]
|
||||
|
||||
## 13.7.1 - 2022-07-13
|
||||
|
||||
* os download: Fix resolving to draft releases [Thodoris Greasidis]
|
||||
|
||||
## 13.7.0 - 2022-07-07
|
||||
|
||||
* Add `--view` flag to `fleet` command for opening a fleet's dashboard page [Matthew Yarmolinsky]
|
||||
|
||||
## 13.6.1 - 2022-06-09
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to use the native OS release phase & variant fields [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-sdk-16.22.0 - 2022-06-06
|
||||
>
|
||||
> * os: Start using the release.phase field in the available versions [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.21.1 - 2022-06-02
|
||||
>
|
||||
> * Add provisioning key expiry date to generateDeviceProvisioningKey [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-16.21.0 - 2022-06-01
|
||||
>
|
||||
> * os: Refactor the computation of OS releases [Thodoris Greasidis]
|
||||
> * os: Use the model's release variant when the native fields are used [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.20.6 - 2022-06-01
|
||||
>
|
||||
> * Deprecate the needsPasswordReset field of the JWTUser [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.20.5 - 2022-05-25
|
||||
>
|
||||
> * Update TypeScript to v4.7 [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 13.6.0 - 2022-06-06
|
||||
|
||||
* Update QEMU to v7.0.0 [Kyle Harding]
|
||||
|
||||
## 13.5.3 - 2022-05-31
|
||||
|
||||
* Drop the needsPasswordReset property from the tests [Thodoris Greasidis]
|
||||
|
||||
## 13.5.2 - 2022-05-31
|
||||
|
||||
* Deduplicate npm-shrinkwrap.json [Thodoris Greasidis]
|
||||
|
||||
## 13.5.1 - 2022-05-26
|
||||
|
||||
* preload: Fix issue where balenaOS v2.98.3+ required an Internet connection to start apps [pipex]
|
||||
|
||||
## 13.5.0 - 2022-05-24
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to 16.20.4 [Nitish Agarwal] </summary>
|
||||
|
||||
> ### balena-sdk-16.20.4 - 2022-05-09
|
||||
>
|
||||
> * bump @types/node from 10.17.60 to 12.20.500 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.20.3 - 2022-05-06
|
||||
>
|
||||
> * patch: bump browserify from 14.5.0 to 17.0.0 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.20.2 - 2022-05-05
|
||||
>
|
||||
> * patch: bump tmp from 0.0.31 to 0.2.1 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.20.1 - 2022-05-05
|
||||
>
|
||||
> * Drop the non-populated apiUrl & actionsUrl properties from Config type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.20.0 - 2022-05-04
|
||||
>
|
||||
> * models.apiKey: Update apiKeyInfo with expiryDate option [Nitish Agarwal]
|
||||
> * os.getConfig: Add typings for the provisioningKeyExpiryDate option [Balena CI]
|
||||
>
|
||||
> ### balena-sdk-16.19.14 - 2022-05-04
|
||||
>
|
||||
> * config.getAll: Mark the deviceTypes property as optional [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.13 - 2022-05-03
|
||||
>
|
||||
> * patch: bump mocha from 3.5.3 to 10.0.0 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.19.12 - 2022-05-03
|
||||
>
|
||||
> * config.getAll: Deprecate the pubnub property and mark as optional [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.11 - 2022-05-03
|
||||
>
|
||||
> * patch: bump mockttp from 0.9.1 to 2.7.0 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.10 - 2022-04-27
|
||||
>
|
||||
> * Reduce the prod typing dependencies [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.9 - 2022-04-26
|
||||
>
|
||||
> * patch: Remove documentation.md from the NPM package [Vipul Gupta]
|
||||
>
|
||||
> ### balena-sdk-16.19.8 - 2022-04-20
|
||||
>
|
||||
> * patch: Remove additional quotes [Vipul Gupta (@vipulgupta2048)]
|
||||
>
|
||||
> ### balena-sdk-16.19.7 - 2022-04-12
|
||||
>
|
||||
> * tests: Update to work with latest major of superagent [Thodoris Greasidis]
|
||||
> * patch: bump superagent from 3.8.3 to 7.1.2 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.19.6 - 2022-04-11
|
||||
>
|
||||
> * patch: bump dotenv from 4.0.0 to 16.0.0 [dependabot[bot]]
|
||||
>
|
||||
> ### balena-sdk-16.19.5 - 2022-04-09
|
||||
>
|
||||
> * Bump karma to v6 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.4 - 2022-04-09
|
||||
>
|
||||
> * Add dependabot configuration [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.3 - 2022-04-06
|
||||
>
|
||||
> * tests: Update v5 model endpoint prefix references [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.19.2 - 2022-04-06
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Fix extracting a meaningful error message instead of "[object Object]" [Thodoris Greasidis] </summary>
|
||||
>
|
||||
>> #### balena-request-11.5.5 - 2022-04-06
|
||||
>>
|
||||
>> * Fix extracting the response error from object response bodies [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-request-11.5.4 - 2022-04-06
|
||||
>>
|
||||
>> * Drop explicit karma-chrome-launcher devDependency [Thodoris Greasidis]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-sdk-16.19.1 - 2022-04-05
|
||||
>
|
||||
> * Update balena-request dependency to v11.5.3 [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.19.0 - 2022-03-16
|
||||
>
|
||||
> * Add release.setKnownIssueList function for setting a release's known issue list [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.18.0 - 2022-03-14
|
||||
>
|
||||
> * minor: Add trying SDK in the browser [Vipul Gupta (@vipulgupta2048)]
|
||||
>
|
||||
> ### balena-sdk-16.17.0 - 2022-03-11
|
||||
>
|
||||
> * device.getWithServiceDetails: Add the release id in the service info [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.16.1 - 2022-03-08
|
||||
>
|
||||
> * Replace internal use of deprecated OsVersion.rawVersion with raw_version [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.16.0 - 2022-03-03
|
||||
>
|
||||
> * Add support for named imports from .mjs files [Thodoris Greasidis]
|
||||
> * Update npx command to fix ts-compatibility tests [Thodoris Greasidis]
|
||||
> * Regenerate Documentation [Thodoris Greasidis]
|
||||
> * Update typescript to 4.6.2 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.15.1 - 2022-02-24
|
||||
>
|
||||
> * Remove unnecessary vpn address filtering when fetching local addresses [Pagan Gazzard]
|
||||
>
|
||||
> ### balena-sdk-16.15.0 - 2022-02-16
|
||||
>
|
||||
> * Add applicationClass parameter to application create function for setting is_of__class property [Matthew Yarmolinsky]
|
||||
>
|
||||
> ### balena-sdk-16.14.0 - 2022-02-15
|
||||
>
|
||||
> * Add name and description field to generateDeviceKey for device. [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-16.13.4 - 2022-01-27
|
||||
>
|
||||
> * typings: Fix conditional $or/$and/$not $filters [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.13.3 - 2022-01-27
|
||||
>
|
||||
> * Deprecate the supportsBlink field of the DeviceTypeJson.DeviceType type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.13.2 - 2022-01-25
|
||||
>
|
||||
> * Deprecate the logoUrl field of the DeviceTypeJson.DeviceType type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.13.1 - 2022-01-21
|
||||
>
|
||||
> * Replace internal use of release.contains__image with release_image [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.13.0 - 2022-01-21
|
||||
>
|
||||
> * models: Deprecate the release.contains__image in favor of the term form [Thodoris Greasidis]
|
||||
> * models: Add the release_image term form property in the Release typings [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.12.1 - 2022-01-17
|
||||
>
|
||||
> * config.getConfigVarSchema: Send the token only when using a device type [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.12.0 - 2022-01-10
|
||||
>
|
||||
> * Replace DeviceTypeJson usage for alias resolution with model queries [Thodoris Greasidis]
|
||||
> * models/device-type: Support aliases as argument of the get() method [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.11.3 - 2022-01-09
|
||||
>
|
||||
> * Fix jsdoc example for balena.errors [Ken Bannister]
|
||||
>
|
||||
> ### balena-sdk-16.11.2 - Invalid date
|
||||
>
|
||||
> * tests: Convert auth spec to async await [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.11.1 - Invalid date
|
||||
>
|
||||
> * Fix buggy tests causing flakiness on node 16 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.11.0 - Invalid date
|
||||
>
|
||||
> * Alias device.getManifestBySlug as config.getDeviceTypeManifestBySlug [Thodoris Greasidis]
|
||||
> * Deprecate device.getManifestByApplication [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.10.0 - Invalid date
|
||||
>
|
||||
> * application.get: Add support for retrieving applications by uuid [Thodoris Greasidis]
|
||||
> * package.json: Rename the lint-fix npm script to lint:fix [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.9.4 - 2021-12-29
|
||||
>
|
||||
> * os: Avoid mutating the args in getAvailableOsVersions & getAllOsVersion [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.9.3 - 2021-12-28
|
||||
>
|
||||
> * os: Replace semver normalization with balena-semver [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.9.2 - 2021-12-28
|
||||
>
|
||||
> * Stop relying on the balena-pine module [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-16.9.1 - 2021-12-28
|
||||
>
|
||||
> * Enable nested changelogs for balena-hup-action-utils [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Add provisioning key expiry date option to config generate options [Balena CI]
|
||||
|
||||
## 13.4.3 - 2022-05-19
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update docker-progress to 5.1.3 [Pagan Gazzard] </summary>
|
||||
|
||||
> ### docker-progress-5.1.3 - 2022-05-11
|
||||
>
|
||||
> * Reject on the stream closing if it has not already ended successfully [Pagan Gazzard]
|
||||
>
|
||||
> ### docker-progress-5.1.2 - 2022-05-10
|
||||
>
|
||||
> * Update dependencies [Pagan Gazzard]
|
||||
>
|
||||
> ### docker-progress-5.1.1 - 2022-05-10
|
||||
>
|
||||
> * Avoid breaking changes to PushPullOptions required properties [Kyle Harding]
|
||||
>
|
||||
> ### docker-progress-5.1.0 - 2022-03-10
|
||||
>
|
||||
> * Add support for building images with progress [Felipe Lalanne]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 13.4.2 - 2022-05-10
|
||||
|
||||
|
||||
<details>
|
||||
<summary> preload: Fix detection of supervisor version for balenaOS v2.93.0 [Kyle Harding] </summary>
|
||||
|
||||
> ### balena-preload-12.0.1 - 2022-05-10
|
||||
>
|
||||
> * Update supervisor image regex to include tagged images [Kyle Harding]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 13.4.1 - 2022-04-11
|
||||
|
||||
* leave: Update log message to advise that device still needs deleting [Taro Murao]
|
||||
|
||||
## 13.4.0 - 2022-04-08
|
||||
|
||||
* deploy: Support all valid semver versions in balena.yml [Thodoris Greasidis]
|
||||
|
||||
## 13.3.3 - 2022-04-08
|
||||
|
||||
* Document the 'patches' folder in CONTRIBUTING.md [Paulo Castro]
|
||||
|
||||
## 13.3.2 - 2022-04-07
|
||||
|
||||
* Skip Alpine tests until Concourse + Alpine v3.14 issues are resolved [Paulo Castro]
|
||||
* build: Ensure HTTPS is used with dockerPort 2376 or with ca/cert/key [Paulo Castro]
|
||||
|
||||
## 13.3.1 - 2022-03-08
|
||||
|
||||
* Include link to Wiki release notes in version update notifications [Paulo Castro]
|
||||
|
||||
## 13.3.0 - 2022-02-28
|
||||
|
||||
* ssh: Allow ssh to service with IP address and production balenaOS image [Paulo Castro]
|
||||
* ssh: Advise use of 'balena login' if root authentication fails [Paulo Castro]
|
||||
|
||||
## 13.2.1 - 2022-02-24
|
||||
|
||||
* Remove unnecessary fetch of device info in `balena tunnel` [Pagan Gazzard]
|
||||
* Correctly use the device uuid when logging the tunnel target [Pagan Gazzard]
|
||||
|
||||
## 13.2.0 - 2022-02-12
|
||||
|
||||
* ssh: Attempt cloud username if 'root' authentication fails [Paulo Castro]
|
||||
* Replace occurrence of through2 dependency with standard stream module [Paulo Castro]
|
||||
* Refactor cached username logic from events.ts to bootstrap.ts for reuse [Paulo Castro]
|
||||
|
||||
## 13.1.13 - 2022-02-10
|
||||
|
||||
* Drop unused awaitDevice utility function [Lucian Buzzo]
|
||||
|
@ -125,39 +125,6 @@ The README file is manually edited, but subsections are automatically extracted
|
||||
|
||||
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
|
||||
## Patches folder
|
||||
|
||||
The `patches` folder contains patch files created with the
|
||||
[patch-package](https://www.npmjs.com/package/patch-package) tool. Small code changes to
|
||||
third-party modules can be made by directly editing Javascript files under the `node_modules`
|
||||
folder and then running `patch-package` to create the patch files. The patch files are then
|
||||
applied immediately after `npm install`, through the `postinstall` script defined in
|
||||
`package.json`.
|
||||
|
||||
The subfolders of the `patches` folder are documented in the
|
||||
[apply-patches.js](https://github.com/balena-io/balena-cli/blob/master/patches/apply-patches.js)
|
||||
script.
|
||||
|
||||
To make changes to the patch files under the `patches` folder, **do not edit them directly,**
|
||||
not even for a "single character change" because the hash values in the patch files also need
|
||||
to be recomputed by `patch-packages`. Instead, edit the relevant files under `node_modules`
|
||||
directly, and then run `patch-packages` with the `--patch-dir` option to specify the subfolder
|
||||
where the patch should be saved. For example, edit `node_modules/exit-hook/index.js` and then
|
||||
run:
|
||||
|
||||
```sh
|
||||
$ npx patch-package --patch-dir patches/all exit-hook
|
||||
```
|
||||
|
||||
That said, these kinds of patches should be avoided in favour of creating pull requests
|
||||
upstream. Patch files create additional maintenance work over time as the patches need to be
|
||||
updated when the dependencies are updated, and they prevent the compounding community benefit
|
||||
that sharing fixes upstream have on open source projects like the balena CLI. The typical
|
||||
scenario where these patches are used is when the upstream maintainers are unresponsive or
|
||||
unwilling to merge the required fixes, the fixes are very small and specific to the balena CLI,
|
||||
and creating a fork of the upstream repo is likely to be more long-term effort than maintaining
|
||||
the patches.
|
||||
|
||||
## Windows
|
||||
|
||||
Besides the regular npm installation dependencies, the `npm run build:installer` script
|
||||
|
@ -78,8 +78,8 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
|
||||
The npm installation involves building native (platform-specific) binary modules, which require
|
||||
some development tools to be installed first, as follows.
|
||||
|
||||
> **The balena CLI currently requires Node.js version 16.**
|
||||
> **Versions 17 and later are not yet fully supported.**
|
||||
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
|
||||
> **Versions 13 and later are not yet fully supported.**
|
||||
|
||||
### Install development tools
|
||||
|
||||
@ -89,7 +89,7 @@ some development tools to be installed first, as follows.
|
||||
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 16
|
||||
$ nvm install 12
|
||||
```
|
||||
|
||||
The `curl` command line above uses
|
||||
@ -106,15 +106,15 @@ recommended.
|
||||
```sh
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 16
|
||||
$ nvm install 12
|
||||
```
|
||||
|
||||
#### **Windows** (not WSL)
|
||||
|
||||
Install:
|
||||
|
||||
* Node.js v12 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||
* If you'd like the ability to switch between Node.js versions, install
|
||||
- Node.js v16 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||
instead.
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||
|
@ -45,6 +45,8 @@ const execFileAsync = promisify(execFile);
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
const MSYS2_BASH =
|
||||
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
|
||||
function dPath(...paths: string[]) {
|
||||
return path.join(ROOT, 'dist', ...paths);
|
||||
@ -87,7 +89,7 @@ async function diffPkgOutput(pkgOut: string) {
|
||||
'tests',
|
||||
'test-data',
|
||||
'pkg',
|
||||
`expected-warnings-${process.platform}-${arch}.txt`,
|
||||
`expected-warnings-${process.platform}.txt`,
|
||||
);
|
||||
const absSavedPath = path.join(ROOT, relSavedPath);
|
||||
const ignoreStartsWith = [
|
||||
@ -180,18 +182,9 @@ async function execPkg(...args: any[]) {
|
||||
* to be directly executed from inside another binary executable.)
|
||||
*/
|
||||
async function buildPkg() {
|
||||
// https://github.com/vercel/pkg#targets
|
||||
let targets = `linux-${arch}`;
|
||||
// TBC: not possible to build for macOS or Windows arm64 on x64 nodes
|
||||
if (process.platform === 'darwin') {
|
||||
targets = `macos-x64`;
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
targets = `win-x64`;
|
||||
}
|
||||
const args = [
|
||||
'--targets',
|
||||
targets,
|
||||
'--target',
|
||||
'host',
|
||||
'--output',
|
||||
'build-bin/balena',
|
||||
'package.json',
|
||||
@ -432,28 +425,20 @@ async function renameInstallerFiles() {
|
||||
|
||||
/**
|
||||
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
||||
* executable installer using Microsoft SignTool.exe (Sign Tool)
|
||||
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
|
||||
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
|
||||
* script (which must be in the PATH) using a MSYS2 bash shell.
|
||||
*/
|
||||
async function signWindowsInstaller() {
|
||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||
const exeName = renamedOclifInstallers[process.platform];
|
||||
console.log(`Signing installer "${exeName}"`);
|
||||
// trust ...
|
||||
await execFileAsync('signtool.exe', [
|
||||
'sign',
|
||||
'-t',
|
||||
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
|
||||
await execFileAsync(MSYS2_BASH, [
|
||||
'sign-exe.sh',
|
||||
'-f',
|
||||
process.env.CSC_LINK,
|
||||
'-p',
|
||||
process.env.CSC_KEY_PASSWORD,
|
||||
exeName,
|
||||
'-d',
|
||||
`balena-cli ${version}`,
|
||||
exeName,
|
||||
]);
|
||||
// ... but verify
|
||||
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping installer signing step because CSC_* env vars are not set',
|
||||
@ -465,21 +450,14 @@ async function signWindowsInstaller() {
|
||||
* Wait for Apple Installer Notarization to continue
|
||||
*/
|
||||
async function notarizeMacInstaller(): Promise<void> {
|
||||
const appleId =
|
||||
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
|
||||
const appBundleId = packageJSON.oclif.macos.identifier || 'io.balena.cli';
|
||||
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
|
||||
|
||||
if (appleIdPassword) {
|
||||
const { notarize } = await import('electron-notarize');
|
||||
// https://github.com/electron/notarize/blob/main/README.md
|
||||
await notarize({
|
||||
appBundleId,
|
||||
appPath: renamedOclifInstallers.darwin,
|
||||
appleId,
|
||||
appleIdPassword,
|
||||
});
|
||||
}
|
||||
const appleId = 'accounts+apple@balena.io';
|
||||
const { notarize } = await import('electron-notarize');
|
||||
await notarize({
|
||||
appBundleId: 'io.balena.etcher',
|
||||
appPath: renamedOclifInstallers.darwin,
|
||||
appleId,
|
||||
appleIdPassword: '@keychain:CLI_PASSWORD',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,12 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// tslint:disable-next-line:import-blacklist
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { simpleGit } from 'simple-git';
|
||||
const stripIndent = require('common-tags/lib/stripIndent');
|
||||
const _ = require('lodash');
|
||||
const { promises: fs } = require('fs');
|
||||
const path = require('path');
|
||||
const simplegit = require('simple-git/promise');
|
||||
|
||||
const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||
|
||||
@ -32,7 +31,7 @@ const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||
* using `touch`.
|
||||
*/
|
||||
async function checkBuildTimestamps() {
|
||||
const git = simpleGit(ROOT);
|
||||
const git = simplegit(ROOT);
|
||||
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
||||
const [docStat, gitStatus] = await Promise.all([
|
||||
fs.stat(docFile),
|
||||
@ -82,5 +81,4 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
run();
|
@ -12,15 +12,15 @@ _balena() {
|
||||
# Sub-completions
|
||||
api_key_cmds=( generate )
|
||||
config_cmds=( generate inject read reconfigure write )
|
||||
device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet )
|
||||
device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown )
|
||||
devices_cmds=( supported )
|
||||
env_cmds=( add rename rm )
|
||||
fleet_cmds=( create pin purge rename restart rm track-latest )
|
||||
fleet_cmds=( create purge rename restart rm )
|
||||
internal_cmds=( osinit )
|
||||
key_cmds=( add rm )
|
||||
local_cmds=( configure flash )
|
||||
os_cmds=( build-config configure download initialize versions )
|
||||
release_cmds=( finalize invalidate validate )
|
||||
release_cmds=( finalize )
|
||||
tag_cmds=( rm set )
|
||||
|
||||
|
||||
|
@ -11,15 +11,15 @@ _balena_complete()
|
||||
# Sub-completions
|
||||
api_key_cmds="generate"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet"
|
||||
device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown"
|
||||
devices_cmds="supported"
|
||||
env_cmds="add rename rm"
|
||||
fleet_cmds="create pin purge rename restart rm track-latest"
|
||||
fleet_cmds="create purge rename restart rm"
|
||||
internal_cmds="osinit"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
release_cmds="finalize invalidate validate"
|
||||
release_cmds="finalize"
|
||||
tag_cmds="rm set"
|
||||
|
||||
|
||||
|
@ -361,7 +361,7 @@ field to sort by (prepend '-' for descending order)
|
||||
|
||||
Display detailed information about a single fleet.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -369,26 +369,23 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena fleet MyFleet
|
||||
$ balena fleet myorg/myfleet
|
||||
$ balena fleet myorg/myfleet --view
|
||||
|
||||
### Arguments
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
#### --view
|
||||
|
||||
open fleet dashboard page
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
@ -443,7 +440,7 @@ fleet device type (Check available types with `balena devices supported`)
|
||||
Purge data from all devices belonging to a fleet.
|
||||
This will clear the fleet's '/data' directory.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -451,7 +448,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -462,7 +461,7 @@ Examples:
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
@ -473,7 +472,7 @@ Rename a fleet.
|
||||
Note, if the `newName` parameter is omitted, it will be
|
||||
prompted for interactively.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -481,7 +480,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -493,7 +494,7 @@ Examples:
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### NEWNAME
|
||||
|
||||
@ -505,7 +506,7 @@ the new name for the fleet
|
||||
|
||||
Restart all devices belonging to a fleet.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -513,7 +514,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -524,7 +527,7 @@ Examples:
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
@ -534,7 +537,7 @@ Permanently remove a fleet.
|
||||
|
||||
The --yes option may be used to avoid interactive confirmation.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -542,7 +545,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -554,7 +559,7 @@ Examples:
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
@ -645,7 +650,7 @@ List all of your devices.
|
||||
|
||||
Devices can be filtered by fleet with the `--fleet` option.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -653,7 +658,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because field names are less likely to change in JSON format and because it
|
||||
@ -671,11 +678,31 @@ Examples:
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
## devices supported
|
||||
|
||||
@ -694,9 +721,29 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
## device <uuid>
|
||||
|
||||
@ -705,7 +752,6 @@ Show information about a single device.
|
||||
Examples:
|
||||
|
||||
$ balena device 7cf02a6
|
||||
$ balena device 7cf02a6 --view
|
||||
|
||||
### Arguments
|
||||
|
||||
@ -715,9 +761,13 @@ the device uuid
|
||||
|
||||
### Options
|
||||
|
||||
#### --view
|
||||
#### --fields FIELDS
|
||||
|
||||
open device dashboard page
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
## device deactivate <uuid>
|
||||
|
||||
@ -783,7 +833,7 @@ If the '--fleet' or '--drive' options are omitted, interactive menus will be
|
||||
presented with values to choose from. If the '--os-version' option is omitted,
|
||||
the latest released OS version for the fleet's default device type will be used.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -791,7 +841,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Image configuration questions will be asked interactively unless a pre-configured
|
||||
'config.json' file is provided with the '--config' option. The file can be
|
||||
@ -801,14 +853,13 @@ Examples:
|
||||
|
||||
$ balena device init
|
||||
$ balena device init -f myorg/myfleet
|
||||
$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes
|
||||
$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes
|
||||
|
||||
### Options
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -y, --yes
|
||||
|
||||
@ -840,10 +891,6 @@ path to the config JSON file, see `balena os build-config`
|
||||
|
||||
custom key name assigned to generated provisioning api key
|
||||
|
||||
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
|
||||
|
||||
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
|
||||
|
||||
## device local-mode <uuid>
|
||||
|
||||
Output current local mode status, or enable/disable local mode
|
||||
@ -882,7 +929,7 @@ Move one or more devices to another fleet.
|
||||
|
||||
If --fleet is omitted, the fleet will be prompted for interactively.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -890,7 +937,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -909,7 +958,7 @@ comma-separated list (no blank spaces) of device UUIDs to be moved
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
## device os-update <uuid>
|
||||
|
||||
@ -923,7 +972,6 @@ Requires balenaCloud; will not work with openBalena or standalone balenaOS.
|
||||
Examples:
|
||||
|
||||
$ balena device os-update 23c73a1
|
||||
$ balena device os-update 23c73a1 --version 2.101.7
|
||||
$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod
|
||||
|
||||
### Arguments
|
||||
@ -948,6 +996,9 @@ This command will output the current public URL for the
|
||||
specified device. It can also enable or disable the URL,
|
||||
or output the enabled status, using the respective options.
|
||||
|
||||
The old command style 'balena device public-url enable <uuid>'
|
||||
is deprecated, but still supported.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device public-url 23c73a1
|
||||
@ -961,6 +1012,10 @@ Examples:
|
||||
|
||||
the uuid of the device to manage
|
||||
|
||||
#### LEGACYUUID
|
||||
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
#### --enable
|
||||
@ -1022,7 +1077,7 @@ Register a new device with a balena fleet.
|
||||
|
||||
If --uuid is not provided, a new UUID will be automatically assigned.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -1030,20 +1085,21 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device register MyFleet
|
||||
$ balena device register MyFleet --uuid <uuid>
|
||||
$ balena device register myorg/myfleet --uuid <uuid>
|
||||
$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>
|
||||
|
||||
### Arguments
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
### Options
|
||||
|
||||
@ -1051,10 +1107,6 @@ fleet name or slug (preferred)
|
||||
|
||||
custom uuid
|
||||
|
||||
#### --deviceType DEVICETYPE
|
||||
|
||||
device type slug (run 'balena devices supported' for possible values)
|
||||
|
||||
## device rename <uuid> [newName]
|
||||
|
||||
Rename a device.
|
||||
@ -1180,6 +1232,14 @@ fleet name or slug (preferred)
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
## release <commitOrId>
|
||||
|
||||
|
||||
@ -1201,6 +1261,14 @@ the commit or ID of the release to get information
|
||||
|
||||
Return the release composition
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
## release finalize <commitOrId>
|
||||
|
||||
Finalize a release. Releases can be "draft" or "final", and this command
|
||||
@ -1261,7 +1329,7 @@ name may be null in JSON output (or 'N/A' in tabular output) if the fleet that
|
||||
the device belonged to is no longer accessible by the current user (for example,
|
||||
in case the current user was removed from the fleet by the fleet's owner).
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -1269,7 +1337,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -1287,7 +1357,7 @@ Examples:
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -c, --config
|
||||
|
||||
@ -1297,9 +1367,29 @@ show configuration variables only
|
||||
|
||||
device UUID
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
produce JSON output instead of tabular output
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
#### -s, --service SERVICE
|
||||
|
||||
@ -1397,7 +1487,7 @@ therefore the --service option cannot be used when the variable name starts
|
||||
with a reserved prefix. When defining custom fleet variables, please avoid
|
||||
these reserved prefixes.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -1405,7 +1495,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -1433,7 +1525,7 @@ variable value; if omitted, use value from this process' environment
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
@ -1516,7 +1608,7 @@ select a service variable (may be used together with the --device option)
|
||||
|
||||
List all tags and their values for the specified fleet, device or release.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -1524,7 +1616,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -1538,7 +1632,7 @@ Examples:
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
@ -1548,11 +1642,35 @@ device UUID
|
||||
|
||||
release id
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
## tag rm <tagKey>
|
||||
|
||||
Remove a tag from a fleet, device or release.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -1560,7 +1678,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -1580,7 +1700,7 @@ the key string of the tag
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
@ -1598,7 +1718,7 @@ You can optionally provide a value to be associated with the created
|
||||
tag, as an extra argument after the tag key. If a value isn't
|
||||
provided, a tag with an empty value is created.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -1606,7 +1726,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -1633,7 +1755,7 @@ the optional value associated with the tag
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
@ -1712,6 +1834,30 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
|
||||
## key <id>
|
||||
|
||||
Display a single SSH key registered in balenaCloud for the logged in user.
|
||||
@ -1728,6 +1874,14 @@ balenaCloud ID for the SSH key
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
## key add <name> [path]
|
||||
|
||||
Add an SSH key to the balenaCloud account of the logged in user.
|
||||
@ -1918,7 +2072,7 @@ Examples:
|
||||
|
||||
#### FLEETORDEVICE
|
||||
|
||||
fleet name/slug, device uuid, or address of local device
|
||||
fleet name/slug/id, device uuid, or address of local device
|
||||
|
||||
#### SERVICE
|
||||
|
||||
@ -1986,7 +2140,7 @@ Examples:
|
||||
|
||||
#### DEVICEORFLEET
|
||||
|
||||
device UUID or fleet name/slug
|
||||
device UUID or fleet name/slug/ID
|
||||
|
||||
### Options
|
||||
|
||||
@ -2072,11 +2226,9 @@ Development images can be selected by appending `.dev` to the version.
|
||||
Examples:
|
||||
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.101.7
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2022.7.0
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.90.0
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2021.10.2.prod
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
|
||||
@ -2160,9 +2312,6 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter
|
||||
being a supervisor feature that allows the "balena push" command to push a user's
|
||||
application directly to a device in the local network.
|
||||
|
||||
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
|
||||
secure boot and disk encryption.
|
||||
|
||||
The --system-connection (-c) option is used to inject NetworkManager connection
|
||||
profiles for additional network interfaces, such as cellular/GSM or additional
|
||||
WiFi or ethernet connections. This option may be passed multiple times in case there
|
||||
@ -2170,7 +2319,7 @@ are multiple files to inject. See connection profile examples and reference at:
|
||||
https://www.balena.io/docs/reference/OS/network/2.x/
|
||||
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -2178,7 +2327,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Note: This command is currently not supported on Windows natively. Windows users
|
||||
are advised to install the Windows Subsystem for Linux (WSL) with Ubuntu, and use
|
||||
@ -2207,7 +2358,7 @@ ask advanced configuration questions (when in interactive mode)
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### --config CONFIG
|
||||
|
||||
@ -2233,10 +2384,6 @@ WiFi SSID (network name) (non-interactive configuration)
|
||||
|
||||
Configure balenaOS to operate in development mode
|
||||
|
||||
#### --secureBoot
|
||||
|
||||
Configure balenaOS installer to opt-in secure boot and disk encryption
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
device UUID
|
||||
@ -2261,16 +2408,10 @@ paths to local files to place into the 'system-connections' directory
|
||||
|
||||
custom key name assigned to generated provisioning api key
|
||||
|
||||
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
|
||||
|
||||
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
|
||||
|
||||
## os initialize <image>
|
||||
|
||||
Initialize an os image for a device with a previously
|
||||
configured operating system image and flash the
|
||||
an external storage drive or the device's storage
|
||||
medium depending on the device type.
|
||||
configured operating system image.
|
||||
|
||||
|
||||
Note: Initializing the device may ask for administrative permissions
|
||||
@ -2321,16 +2462,13 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter
|
||||
being a supervisor feature that allows the "balena push" command to push a user's
|
||||
application directly to a device in the local network.
|
||||
|
||||
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
|
||||
secure boot and disk encryption.
|
||||
|
||||
To configure an image for a fleet of mixed device types, use the --fleet option
|
||||
alongside the --deviceType option to specify the target device type.
|
||||
|
||||
To avoid interactive questions, specify a command line option for each question that
|
||||
would otherwise be asked.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -2338,7 +2476,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -2347,7 +2487,6 @@ Examples:
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
|
||||
$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev
|
||||
$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot
|
||||
$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3
|
||||
$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json
|
||||
$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15
|
||||
@ -2360,16 +2499,12 @@ a balenaOS version
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### --dev
|
||||
|
||||
Configure balenaOS to operate in development mode
|
||||
|
||||
#### --secureBoot
|
||||
|
||||
Configure balenaOS installer to opt-in secure boot and disk encryption
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
device UUID
|
||||
@ -2410,10 +2545,6 @@ supervisor cloud polling interval in minutes (e.g. for device variables)
|
||||
|
||||
custom key name assigned to generated provisioning api key
|
||||
|
||||
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
|
||||
|
||||
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
|
||||
|
||||
## config inject <file>
|
||||
|
||||
Inject a 'config.json' file to a balenaOS image file or attached SD card or
|
||||
@ -2540,7 +2671,7 @@ Check also the Preloading and Preregistering section of the balena CLI's advance
|
||||
masterclass document:
|
||||
https://www.balena.io/docs/learn/more/masterclasses/advanced-cli/#5-preloading-and-preregistering
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -2548,7 +2679,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Note that the this command requires Docker to be installed, as further detailed
|
||||
in the balena CLI's installation instructions:
|
||||
@ -2574,7 +2707,7 @@ the image file path
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -c, --commit COMMIT
|
||||
|
||||
@ -2738,7 +2871,6 @@ Examples:
|
||||
$ balena push myFleet
|
||||
$ balena push myFleet --source <source directory>
|
||||
$ balena push myFleet -s <source directory>
|
||||
$ balena push myFleet --source <source directory> --note "this is the note for this release"
|
||||
$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"
|
||||
$ balena push myorg/myfleet
|
||||
|
||||
@ -2803,7 +2935,7 @@ used (usually $HOME/.balena/secrets.yml|.json)
|
||||
|
||||
Don't run a live session on this push. The filesystem will not be monitored,
|
||||
and changes will not be synchronized to any running containers. Note that both
|
||||
this flag and --detached are required to cause the process to end once the
|
||||
this flag and --detached and required to cause the process to end once the
|
||||
initial build has completed.
|
||||
|
||||
#### -d, --detached
|
||||
@ -2855,10 +2987,6 @@ by the 'track latest' release policy but can be used through release pinning.
|
||||
Draft releases can be marked as final through the API. Releases are created
|
||||
as final by default unless this option is given.
|
||||
|
||||
#### --note NOTE
|
||||
|
||||
The notes for this release
|
||||
|
||||
# Settings
|
||||
|
||||
## settings
|
||||
@ -2871,6 +2999,14 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
# Local
|
||||
|
||||
## local configure <target>
|
||||
@ -3038,7 +3174,7 @@ the type of device this build is for
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -e, --emulated
|
||||
|
||||
@ -3048,6 +3184,10 @@ Use QEMU for ARM architecture emulation during the image build
|
||||
|
||||
Alternative Dockerfile name/path, relative to the source folder
|
||||
|
||||
#### --logs
|
||||
|
||||
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
|
||||
|
||||
#### --nologs
|
||||
|
||||
Hide the image build log output (produce less verbose output)
|
||||
@ -3220,7 +3360,6 @@ Examples:
|
||||
|
||||
$ balena deploy myFleet
|
||||
$ balena deploy myorg/myfleet --build --source myBuildDir/
|
||||
$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"
|
||||
$ balena deploy myorg/myfleet myRepo/myImage
|
||||
$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"
|
||||
|
||||
@ -3228,7 +3367,7 @@ Examples:
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### IMAGE
|
||||
|
||||
@ -3261,10 +3400,6 @@ by the 'track latest' release policy but can be used through release pinning.
|
||||
Draft releases can be marked as final through the API. Releases are created
|
||||
as final by default unless this option is given.
|
||||
|
||||
#### --note NOTE
|
||||
|
||||
The notes for this release
|
||||
|
||||
#### -e, --emulated
|
||||
|
||||
Use QEMU for ARM architecture emulation during the image build
|
||||
@ -3273,6 +3408,10 @@ Use QEMU for ARM architecture emulation during the image build
|
||||
|
||||
Alternative Dockerfile name/path, relative to the source folder
|
||||
|
||||
#### --logs
|
||||
|
||||
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
|
||||
|
||||
#### --nologs
|
||||
|
||||
Hide the image build log output (produce less verbose output)
|
||||
@ -3366,7 +3505,7 @@ scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This may require administrator/root privileges.
|
||||
Likewise, if the fleet option is not provided then a picker will be shown.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -3374,7 +3513,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -3395,7 +3536,7 @@ the IP or hostname of device
|
||||
|
||||
#### -f, --fleet FLEET
|
||||
|
||||
fleet name or slug (preferred)
|
||||
fleet name, slug (preferred), or numeric ID (deprecated)
|
||||
|
||||
#### -i, --pollInterval POLLINTERVAL
|
||||
|
||||
@ -3451,7 +3592,7 @@ or hours, e.g. '12h', '2d'.
|
||||
Both --device and --fleet flags accept multiple values, specified as
|
||||
a comma-separated list (with no spaces).
|
||||
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the `balena fleets` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -3459,7 +3600,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.
|
||||
|
||||
Examples:
|
||||
|
||||
|
@ -171,4 +171,5 @@ export default abstract class BalenaCommand extends Command {
|
||||
|
||||
protected outputMessage = output.outputMessage;
|
||||
protected outputData = output.outputData;
|
||||
protected printTitle = output.printTitle;
|
||||
}
|
||||
|
@ -19,18 +19,13 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
devModeInfo,
|
||||
secureBootInfo,
|
||||
} from '../../utils/messages';
|
||||
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
|
||||
import type { PineDeferred } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
version: string; // OS version
|
||||
fleet?: string;
|
||||
dev?: boolean; // balenaOS development variant
|
||||
secureBoot?: boolean;
|
||||
device?: string;
|
||||
deviceApiKey?: string;
|
||||
deviceType?: string;
|
||||
@ -42,7 +37,6 @@ interface FlagsDef {
|
||||
wifiKey?: string;
|
||||
appUpdatePollInterval?: string;
|
||||
'provisioning-key-name'?: string;
|
||||
'provisioning-key-expiry-date'?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -56,8 +50,6 @@ export default class ConfigGenerateCmd extends Command {
|
||||
|
||||
${devModeInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
${secureBootInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
To configure an image for a fleet of mixed device types, use the --fleet option
|
||||
alongside the --deviceType option to specify the target device type.
|
||||
|
||||
@ -73,7 +65,6 @@ export default class ConfigGenerateCmd extends Command {
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
||||
@ -88,14 +79,9 @@ export default class ConfigGenerateCmd extends Command {
|
||||
}),
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
dev: cf.dev,
|
||||
secureBoot: cf.secureBoot,
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: [
|
||||
'fleet',
|
||||
'provisioning-key-name',
|
||||
'provisioning-key-expiry-date',
|
||||
],
|
||||
exclusive: ['fleet', 'provisioning-key-name'],
|
||||
},
|
||||
deviceApiKey: flags.string({
|
||||
description:
|
||||
@ -134,11 +120,6 @@ export default class ConfigGenerateCmd extends Command {
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
'provisioning-key-expiry-date': flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -159,9 +140,11 @@ export default class ConfigGenerateCmd extends Command {
|
||||
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
||||
| null = null;
|
||||
if (options.device != null) {
|
||||
const rawDevice = await balena.models.device.get(options.device, {
|
||||
$expand: { is_of__device_type: { $select: 'slug' } },
|
||||
});
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const rawDevice = await balena.models.device.get(
|
||||
tryAsInteger(options.device),
|
||||
{ $expand: { is_of__device_type: { $select: 'slug' } } },
|
||||
);
|
||||
if (!rawDevice.belongs_to__application) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(stripIndent`
|
||||
@ -184,34 +167,25 @@ export default class ConfigGenerateCmd extends Command {
|
||||
|
||||
const deviceType = options.deviceType || resourceDeviceType;
|
||||
|
||||
// Check compatibility if application and deviceType provided
|
||||
if (options.fleet && options.deviceType) {
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (
|
||||
!(await helpers.areDeviceTypesCompatible(
|
||||
resourceDeviceType,
|
||||
deviceType,
|
||||
))
|
||||
) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(
|
||||
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deviceManifest = await balena.models.device.getManifestBySlug(
|
||||
deviceType,
|
||||
);
|
||||
|
||||
const { validateSecureBootOptionAndWarn } = await import(
|
||||
'../../utils/config'
|
||||
);
|
||||
await validateSecureBootOptionAndWarn(
|
||||
options.secureBoot,
|
||||
deviceType,
|
||||
options.version,
|
||||
);
|
||||
// Check compatibility if application and deviceType provided
|
||||
if (options.fleet && options.deviceType) {
|
||||
const appDeviceManifest = await balena.models.device.getManifestBySlug(
|
||||
resourceDeviceType,
|
||||
);
|
||||
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (
|
||||
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
|
||||
) {
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for values
|
||||
// Pass params as an override: if there is any param with exactly the same name as a
|
||||
@ -221,9 +195,7 @@ export default class ConfigGenerateCmd extends Command {
|
||||
});
|
||||
answers.version = options.version;
|
||||
answers.developmentMode = options.dev;
|
||||
answers.secureBoot = options.secureBoot;
|
||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
||||
|
||||
// Generate config
|
||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||
|
@ -57,6 +57,7 @@ export default class ConfigInjectCmd extends Command {
|
||||
public static usage = 'config inject <file>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
@ -47,6 +47,7 @@ export default class ConfigReadCmd extends Command {
|
||||
public static usage = 'config read';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
|
@ -50,6 +50,7 @@ export default class ConfigReconfigureCmd extends Command {
|
||||
public static usage = 'config reconfigure';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
advanced: flags.boolean({
|
||||
description: 'show advanced commands',
|
||||
|
@ -64,6 +64,7 @@ export default class ConfigWriteCmd extends Command {
|
||||
public static usage = 'config write <key> <value>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { ImageDescriptor } from '@balena/compose/dist/parse';
|
||||
import type { ImageDescriptor } from 'resin-compose-parse';
|
||||
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
@ -60,7 +60,6 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
nologupload: boolean;
|
||||
'release-tag'?: string[];
|
||||
draft: boolean;
|
||||
note?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -102,7 +101,6 @@ ${dockerignoreHelp}
|
||||
public static examples = [
|
||||
'$ balena deploy myFleet',
|
||||
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
|
||||
'$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"',
|
||||
'$ balena deploy myorg/myfleet myRepo/myImage',
|
||||
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||
];
|
||||
@ -147,7 +145,6 @@ ${dockerignoreHelp}
|
||||
as final by default unless this option is given.`,
|
||||
default: false,
|
||||
}),
|
||||
note: flags.string({ description: 'The notes for this release' }),
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
@ -234,9 +231,6 @@ ${dockerignoreHelp}
|
||||
releaseTagKeys,
|
||||
releaseTagValues,
|
||||
);
|
||||
if (options.note) {
|
||||
await sdk.models.release.note(release.id, options.note);
|
||||
}
|
||||
}
|
||||
|
||||
async deployProject(
|
||||
@ -340,7 +334,7 @@ ${dockerignoreHelp}
|
||||
);
|
||||
|
||||
let release: Release | ComposeReleaseInfo['release'];
|
||||
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
|
||||
if (appType?.is_legacy) {
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
|
||||
const msg = getChalk().yellow(
|
||||
|
@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
interface FlagsDef {
|
||||
@ -42,6 +43,7 @@ export default class DeviceIdentifyCmd extends Command {
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to identify',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,8 +21,11 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { Application, Release } from 'balena-sdk';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
@ -41,9 +44,8 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
undervoltage_detected?: boolean;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
view: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -56,15 +58,13 @@ export default class DeviceCmd extends Command {
|
||||
|
||||
Show information about a single device.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device 7cf02a6',
|
||||
'$ balena device 7cf02a6 --view',
|
||||
];
|
||||
public static examples = ['$ balena device 7cf02a6'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the device uuid',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
@ -73,10 +73,7 @@ export default class DeviceCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
view: flags.boolean({
|
||||
default: false,
|
||||
description: 'open device dashboard page',
|
||||
}),
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
@ -116,14 +113,6 @@ export default class DeviceCmd extends Command {
|
||||
],
|
||||
...expandForAppName,
|
||||
})) as ExtendedDevice;
|
||||
|
||||
if (options.view) {
|
||||
const open = await import('open');
|
||||
const dashboardUrl = balena.models.device.getDashboardUrl(device.uuid);
|
||||
await open(dashboardUrl, { wait: false });
|
||||
return;
|
||||
}
|
||||
|
||||
device.status = device.overall_status;
|
||||
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||
@ -179,37 +168,52 @@ export default class DeviceCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
'fleet',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
'cpu_usage_percent',
|
||||
'cpu_temp_c',
|
||||
'cpu_id',
|
||||
'memory_usage_mb',
|
||||
'memory_total_mb',
|
||||
'memory_usage_percent',
|
||||
'storage_block_device',
|
||||
'storage_usage_mb',
|
||||
'storage_total_mb',
|
||||
'storage_usage_percent',
|
||||
'undervoltage_detected',
|
||||
]),
|
||||
);
|
||||
const outputFields = [
|
||||
'device_name',
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
'fleet',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
'cpu_usage_percent',
|
||||
'cpu_temp_c',
|
||||
'cpu_id',
|
||||
'memory_usage_mb',
|
||||
'memory_total_mb',
|
||||
'memory_usage_percent',
|
||||
'storage_block_device',
|
||||
'storage_usage_mb',
|
||||
'storage_total_mb',
|
||||
'storage_usage_percent',
|
||||
'undervoltage_detected',
|
||||
];
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(device, outputFields, {
|
||||
...options,
|
||||
hideNullOrUndefinedValues: true,
|
||||
titleField: 'device_name',
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
outputFields.unshift(`$${device.device_name}$`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(
|
||||
device,
|
||||
outputFields.filter((f) => f !== 'device_name'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ interface FlagsDef {
|
||||
config?: string;
|
||||
help: void;
|
||||
'provisioning-key-name'?: string;
|
||||
'provisioning-key-expiry-date'?: string;
|
||||
}
|
||||
|
||||
export default class DeviceInitCmd extends Command {
|
||||
@ -70,7 +69,6 @@ export default class DeviceInitCmd extends Command {
|
||||
public static examples = [
|
||||
'$ balena device init',
|
||||
'$ balena device init -f myorg/myfleet',
|
||||
'$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes',
|
||||
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes',
|
||||
];
|
||||
|
||||
@ -99,10 +97,6 @@ export default class DeviceInitCmd extends Command {
|
||||
'provisioning-key-name': flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
}),
|
||||
'provisioning-key-expiry-date': flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -129,7 +123,7 @@ export default class DeviceInitCmd extends Command {
|
||||
options.fleet ||
|
||||
(
|
||||
await (await import('../../utils/patterns')).selectApplication()
|
||||
).slug,
|
||||
).id,
|
||||
{
|
||||
$expand: {
|
||||
is_for__device_type: {
|
||||
@ -191,14 +185,6 @@ export default class DeviceInitCmd extends Command {
|
||||
options['provisioning-key-name'],
|
||||
);
|
||||
}
|
||||
|
||||
if (options['provisioning-key-expiry-date']) {
|
||||
configureCommand.push(
|
||||
'--provisioning-key-expiry-date',
|
||||
options['provisioning-key-expiry-date'],
|
||||
);
|
||||
}
|
||||
|
||||
await runCommand(configureCommand);
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
enable: boolean;
|
||||
@ -51,6 +52,7 @@ export default class DeviceLocalModeCmd extends Command {
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to manage',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
@ -21,7 +21,6 @@ import type {
|
||||
BalenaSDK,
|
||||
Device,
|
||||
DeviceType,
|
||||
PineOptions,
|
||||
PineTypedResult,
|
||||
} from 'balena-sdk';
|
||||
import Command from '../../command';
|
||||
@ -89,14 +88,17 @@ export default class DeviceMoveCmd extends Command {
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
|
||||
|
||||
// Split uuids string into array of uuids
|
||||
const deviceUuids = params.uuid.split(',');
|
||||
// Parse ids string into array of correct types
|
||||
const deviceIds: Array<string | number> = params.uuid
|
||||
.split(',')
|
||||
.map((id) => tryAsInteger(id));
|
||||
|
||||
// Get devices
|
||||
const devices = await Promise.all(
|
||||
deviceUuids.map(
|
||||
deviceIds.map(
|
||||
(uuid) =>
|
||||
balena.models.device.get(
|
||||
uuid,
|
||||
@ -113,7 +115,7 @@ export default class DeviceMoveCmd extends Command {
|
||||
: 'N/a';
|
||||
}
|
||||
|
||||
// Disambiguate application
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
// Get destination application
|
||||
@ -122,7 +124,7 @@ export default class DeviceMoveCmd extends Command {
|
||||
: await this.interactivelySelectApplication(balena, devices);
|
||||
|
||||
// Move each device
|
||||
for (const uuid of deviceUuids) {
|
||||
for (const uuid of deviceIds) {
|
||||
try {
|
||||
await balena.models.device.move(uuid, application.id);
|
||||
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
|
||||
@ -154,7 +156,7 @@ export default class DeviceMoveCmd extends Command {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
} satisfies PineOptions<DeviceType>;
|
||||
} as const;
|
||||
const deviceTypes = (await balena.models.deviceType.getAllSupported(
|
||||
deviceTypeOptions,
|
||||
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
|
||||
|
@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { Device } from 'balena-sdk';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
@ -46,7 +47,6 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device os-update 23c73a1',
|
||||
'$ balena device os-update 23c73a1 --version 2.101.7',
|
||||
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
|
||||
];
|
||||
|
||||
@ -54,6 +54,7 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to update',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
@ -1,103 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { getExpandedProp } from '../../utils/pine';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
releaseToPinTo?: string;
|
||||
}
|
||||
|
||||
export default class DevicePinCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Pin a device to a release.
|
||||
|
||||
Pin a device to a release.
|
||||
|
||||
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device pin 7cf02a6',
|
||||
'$ balena device pin 7cf02a6 91165e5',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to pin to a release',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseToPinTo',
|
||||
description: 'the commit of the release for the device to get pinned to',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device pin <uuid> [releaseToPinTo]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePinCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const device = await balena.models.device.get(params.uuid, {
|
||||
$expand: {
|
||||
should_be_running__release: {
|
||||
$select: 'commit',
|
||||
},
|
||||
belongs_to__application: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pinnedRelease = getExpandedProp(
|
||||
device.should_be_running__release,
|
||||
'commit',
|
||||
);
|
||||
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
|
||||
|
||||
const releaseToPinTo = params.releaseToPinTo;
|
||||
|
||||
if (!releaseToPinTo) {
|
||||
console.log(
|
||||
`${
|
||||
pinnedRelease
|
||||
? `This device is currently pinned to ${pinnedRelease}.`
|
||||
: 'This device is not currently pinned to any release.'
|
||||
} \n\nTo see a list of all releases this device can be pinned to, run \`balena releases ${appSlug}\`.`,
|
||||
);
|
||||
} else {
|
||||
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);
|
||||
}
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
enable: boolean;
|
||||
@ -31,6 +32,8 @@ interface FlagsDef {
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
// Optional hidden arg to support old command format
|
||||
legacyUuid?: string;
|
||||
}
|
||||
|
||||
export default class DevicePublicUrlCmd extends Command {
|
||||
@ -40,6 +43,9 @@ export default class DevicePublicUrlCmd extends Command {
|
||||
This command will output the current public URL for the
|
||||
specified device. It can also enable or disable the URL,
|
||||
or output the enabled status, using the respective options.
|
||||
|
||||
The old command style 'balena device public-url enable <uuid>'
|
||||
is deprecated, but still supported.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
@ -53,8 +59,15 @@ export default class DevicePublicUrlCmd extends Command {
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to manage',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
// Optional hidden arg to support old command format
|
||||
name: 'legacyUuid',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device public-url <uuid>';
|
||||
@ -82,6 +95,25 @@ export default class DevicePublicUrlCmd extends Command {
|
||||
DevicePublicUrlCmd,
|
||||
);
|
||||
|
||||
// Legacy command format support.
|
||||
// Previously this command used the following format
|
||||
// (changed due to oclif technicalities):
|
||||
// `balena device public-url enable|disable|status <uuid>`
|
||||
if (params.legacyUuid) {
|
||||
const action = params.uuid;
|
||||
if (!['enable', 'disable', 'status'].includes(action)) {
|
||||
throw new ExpectedError(
|
||||
`Unexpected arguments: ${params.uuid} ${params.legacyUuid}`,
|
||||
);
|
||||
}
|
||||
|
||||
options.enable = action === 'enable';
|
||||
options.disable = action === 'disable';
|
||||
options.status = action === 'status';
|
||||
params.uuid = params.legacyUuid;
|
||||
delete params.legacyUuid;
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if (options.enable) {
|
||||
|
@ -63,14 +63,17 @@ export default class DevicePurgeCmd extends Command {
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const deviceUuids = params.uuid.split(',');
|
||||
const deviceIds = params.uuid.split(',').map((id) => {
|
||||
return tryAsInteger(id);
|
||||
});
|
||||
|
||||
for (const uuid of deviceUuids) {
|
||||
ux.action.start(`Purging data from device ${uuid}`);
|
||||
await balena.models.device.purge(uuid);
|
||||
for (const deviceId of deviceIds) {
|
||||
ux.action.start(`Purging data from device ${deviceId}`);
|
||||
await balena.models.device.purge(deviceId);
|
||||
ux.action.stop();
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
force: boolean;
|
||||
@ -42,6 +43,7 @@ export default class DeviceRebootCmd extends Command {
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to reboot',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
@ -25,7 +25,6 @@ import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
uuid?: string;
|
||||
deviceType?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -48,7 +47,6 @@ export default class DeviceRegisterCmd extends Command {
|
||||
'$ balena device register MyFleet',
|
||||
'$ balena device register MyFleet --uuid <uuid>',
|
||||
'$ balena device register myorg/myfleet --uuid <uuid>',
|
||||
'$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [ca.fleetRequired];
|
||||
@ -60,10 +58,6 @@ export default class DeviceRegisterCmd extends Command {
|
||||
description: 'custom uuid',
|
||||
char: 'u',
|
||||
}),
|
||||
deviceType: flags.string({
|
||||
description:
|
||||
"device type slug (run 'balena devices supported' for possible values)",
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -83,11 +77,7 @@ export default class DeviceRegisterCmd extends Command {
|
||||
|
||||
console.info(`Registering to ${application.slug}: ${uuid}`);
|
||||
|
||||
const result = await balena.models.device.register(
|
||||
application.id,
|
||||
uuid,
|
||||
options.deviceType,
|
||||
);
|
||||
const result = await balena.models.device.register(application.id, uuid);
|
||||
|
||||
return result && result.uuid;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
@ -47,6 +48,7 @@ export default class DeviceRenameCmd extends Command {
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to rename',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
|
@ -82,21 +82,24 @@ export default class DeviceRestartCmd extends Command {
|
||||
DeviceRestartCmd,
|
||||
);
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const deviceUuids = params.uuid.split(',');
|
||||
const deviceIds = params.uuid.split(',').map((id) => {
|
||||
return tryAsInteger(id);
|
||||
});
|
||||
const serviceNames = options.service?.split(',');
|
||||
|
||||
// Iterate sequentially through deviceUuids.
|
||||
// Iterate sequentially through deviceIds.
|
||||
// We may later want to add a batching feature,
|
||||
// so that n devices are processed in parallel
|
||||
for (const uuid of deviceUuids) {
|
||||
ux.action.start(`Restarting services on device ${uuid}`);
|
||||
for (const deviceId of deviceIds) {
|
||||
ux.action.start(`Restarting services on device ${deviceId}`);
|
||||
if (serviceNames) {
|
||||
await this.restartServices(balena, uuid, serviceNames);
|
||||
await this.restartServices(balena, deviceId, serviceNames);
|
||||
} else {
|
||||
await this.restartAllServices(balena, uuid);
|
||||
await this.restartAllServices(balena, deviceId);
|
||||
}
|
||||
ux.action.stop();
|
||||
}
|
||||
@ -104,7 +107,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
|
||||
async restartServices(
|
||||
balena: BalenaSDK,
|
||||
deviceUuid: string,
|
||||
deviceId: number | string,
|
||||
serviceNames: string[],
|
||||
) {
|
||||
const { ExpectedError, instanceOf } = await import('../../errors');
|
||||
@ -113,7 +116,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
// Get device
|
||||
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
|
||||
try {
|
||||
device = await balena.models.device.getWithServiceDetails(deviceUuid, {
|
||||
device = await balena.models.device.getWithServiceDetails(deviceId, {
|
||||
$expand: {
|
||||
is_running__release: { $select: 'commit' },
|
||||
},
|
||||
@ -121,7 +124,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
} catch (e) {
|
||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||
throw new ExpectedError(`Device ${deviceUuid} not found.`);
|
||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
@ -133,7 +136,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
serviceNames.forEach((service) => {
|
||||
if (!device.current_services[service]) {
|
||||
throw new ExpectedError(
|
||||
`Service ${service} not found on device ${deviceUuid}.`,
|
||||
`Service ${service} not found on device ${deviceId}.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -152,7 +155,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
if (serviceContainer) {
|
||||
restartPromises.push(
|
||||
balena.models.device.restartService(
|
||||
deviceUuid,
|
||||
deviceId,
|
||||
serviceContainer.image_id,
|
||||
),
|
||||
);
|
||||
@ -163,32 +166,32 @@ export default class DeviceRestartCmd extends Command {
|
||||
await Promise.all(restartPromises);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('no online device')) {
|
||||
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||
throw new ExpectedError(`Device ${deviceId} is not online.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
|
||||
async restartAllServices(balena: BalenaSDK, deviceId: number | string) {
|
||||
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
|
||||
// Need to use device.get first to distinguish between non-existant and offline devices.
|
||||
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
|
||||
const { instanceOf, ExpectedError } = await import('../../errors');
|
||||
try {
|
||||
const device = await balena.models.device.get(deviceUuid);
|
||||
const device = await balena.models.device.get(deviceId);
|
||||
if (!device.is_online) {
|
||||
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||
throw new ExpectedError(`Device ${deviceId} is not online.`);
|
||||
}
|
||||
} catch (e) {
|
||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||
throw new ExpectedError(`Device ${deviceUuid} not found.`);
|
||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await balena.models.device.restartApplication(deviceUuid);
|
||||
await balena.models.device.restartApplication(deviceId);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
@ -83,7 +84,7 @@ export default class DeviceRmCmd extends Command {
|
||||
// Remove
|
||||
for (const uuid of uuids) {
|
||||
try {
|
||||
await balena.models.device.remove(uuid);
|
||||
await balena.models.device.remove(tryAsInteger(uuid));
|
||||
} catch (err) {
|
||||
console.info(`${err.message}, uuid: ${uuid}`);
|
||||
process.exitCode = 1;
|
||||
|
@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
interface FlagsDef {
|
||||
@ -43,6 +44,7 @@ export default class DeviceShutdownCmd extends Command {
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to shutdown',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
@ -1,63 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceTrackFleetCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Make a device track the fleet's pinned release.
|
||||
|
||||
Make a device track the fleet's pinned release.
|
||||
`;
|
||||
public static examples = ['$ balena device track-fleet 7cf02a6'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: "the uuid of the device to make track the fleet's release",
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device track-fleet <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceTrackFleetCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
await balena.models.device.trackApplicationRelease(params.uuid);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,8 +21,10 @@ import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||
import type { Application } from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../../framework';
|
||||
|
||||
import type { Application, Device, PineOptions } from 'balena-sdk';
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
@ -30,24 +32,12 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
device_type?: string | null;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
const devicesSelectFields = {
|
||||
$select: [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
],
|
||||
} satisfies PineOptions<Device>;
|
||||
|
||||
export default class DevicesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all devices.
|
||||
@ -70,19 +60,17 @@ export default class DevicesCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
fleet: cf.fleet,
|
||||
json: cf.json,
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const devicesOptions = { ...devicesSelectFields, ...expandForAppName };
|
||||
|
||||
let devices;
|
||||
|
||||
@ -91,11 +79,11 @@ export default class DevicesCmd extends Command {
|
||||
const application = await getApplication(balena, options.fleet);
|
||||
devices = (await balena.models.device.getAllByApplication(
|
||||
application.id,
|
||||
devicesOptions,
|
||||
expandForAppName,
|
||||
)) as ExtendedDevice[];
|
||||
} else {
|
||||
devices = (await balena.models.device.getAll(
|
||||
devicesOptions,
|
||||
expandForAppName,
|
||||
)) as ExtendedDevice[];
|
||||
}
|
||||
|
||||
@ -112,31 +100,52 @@ export default class DevicesCmd extends Command {
|
||||
return device;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
if (isV14()) {
|
||||
const outputFields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
await this.outputData(devices, outputFields, {
|
||||
...options,
|
||||
displayNullValuesAs: 'N/a',
|
||||
});
|
||||
} else {
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
// Old output implementation
|
||||
const fields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,15 +15,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import type { DataSetOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
json?: boolean;
|
||||
}
|
||||
@ -52,54 +53,55 @@ export default class DevicesSupportedCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
||||
const pineOptions = {
|
||||
$select: ['slug', 'name'],
|
||||
$expand: {
|
||||
is_of__cpu_architecture: { $select: 'slug' },
|
||||
device_type_alias: {
|
||||
$select: 'is_referenced_by__alias',
|
||||
$orderby: { is_referenced_by__alias: 'asc' },
|
||||
},
|
||||
},
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
|
||||
const dts = (await getBalenaSdk().models.deviceType.getAllSupported(
|
||||
pineOptions,
|
||||
)) as Array<
|
||||
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
|
||||
>;
|
||||
const [dts, configDTs] = await Promise.all([
|
||||
getBalenaSdk().models.deviceType.getAllSupported({
|
||||
$expand: { is_of__cpu_architecture: { $select: 'slug' } },
|
||||
$select: ['slug', 'name'],
|
||||
}),
|
||||
getBalenaSdk().models.config.getDeviceTypes(),
|
||||
]);
|
||||
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
|
||||
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
|
||||
interface DT {
|
||||
slug: string;
|
||||
aliases: string[];
|
||||
aliases: string[] | string;
|
||||
arch: string;
|
||||
name: string;
|
||||
}
|
||||
let deviceTypes = dts.map((dt): DT => {
|
||||
const aliases = dt.device_type_alias
|
||||
.map((dta) => dta.is_referenced_by__alias)
|
||||
.filter((alias) => alias !== dt.slug);
|
||||
return {
|
||||
slug: dt.slug,
|
||||
aliases: options.json ? aliases : [aliases.join(', ')],
|
||||
arch: dt.is_of__cpu_architecture[0]?.slug || 'n/a',
|
||||
let deviceTypes: DT[] = [];
|
||||
for (const slug of Object.keys(dtsBySlug)) {
|
||||
const configDT: Partial<typeof configDTs[0]> =
|
||||
configDTsBySlug[slug] || {};
|
||||
const aliases = (configDT.aliases || []).filter(
|
||||
(alias) => alias !== slug,
|
||||
);
|
||||
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
|
||||
deviceTypes.push({
|
||||
slug,
|
||||
aliases: options.json ? aliases : aliases.join(', '),
|
||||
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
|
||||
name: dt.name || 'N/A',
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
const fields = ['slug', 'aliases', 'arch', 'name'];
|
||||
deviceTypes = _.sortBy(deviceTypes, fields);
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
|
||||
if (isV14()) {
|
||||
await this.outputData(deviceTypes, fields, options);
|
||||
} else {
|
||||
const visuals = getVisuals();
|
||||
const output = await visuals.table.horizontal(deviceTypes, fields);
|
||||
console.log(output);
|
||||
// Old output implementation
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
} else {
|
||||
const visuals = getVisuals();
|
||||
const output = await visuals.table.horizontal(deviceTypes, fields);
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -22,12 +22,15 @@ import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
config: boolean;
|
||||
device?: string; // device UUID
|
||||
json: boolean;
|
||||
json?: boolean;
|
||||
help: void;
|
||||
service?: string; // service name
|
||||
}
|
||||
@ -113,7 +116,7 @@ export default class EnvsCmd extends Command {
|
||||
}),
|
||||
device: { ...cf.device, exclusive: ['fleet'] },
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
|
||||
service: { ...cf.service, exclusive: ['config'] },
|
||||
};
|
||||
|
||||
@ -181,24 +184,59 @@ export default class EnvsCmd extends Command {
|
||||
return i;
|
||||
});
|
||||
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
if (!options.config) {
|
||||
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
|
||||
}
|
||||
if (isV14()) {
|
||||
const results = [...varArray] as any;
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../utils/helpers');
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
// Rename fields
|
||||
if (options.device) {
|
||||
if (options.json) {
|
||||
fields.push('deviceUUID');
|
||||
} else {
|
||||
results.forEach((r: any) => {
|
||||
r.device = r.deviceUUID;
|
||||
delete r.deviceUUID;
|
||||
});
|
||||
|
||||
fields.push('device');
|
||||
}
|
||||
}
|
||||
if (!options.config) {
|
||||
if (options.json) {
|
||||
fields.push('serviceName');
|
||||
} else {
|
||||
results.forEach((r: any) => {
|
||||
r.service = r.serviceName;
|
||||
delete r.serviceName;
|
||||
});
|
||||
fields.push('service');
|
||||
}
|
||||
}
|
||||
|
||||
await this.outputData(results, fields, {
|
||||
...options,
|
||||
sort: options.sort || 'name',
|
||||
});
|
||||
} else {
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
// Old output implementation
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
if (!options.config) {
|
||||
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
const { pickAndRename } = await import('../utils/helpers');
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,20 +15,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { flags as flagsType } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { Release } from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import { isV14 } from '../../utils/version';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
view: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -46,20 +45,15 @@ export default class FleetCmd extends Command {
|
||||
public static examples = [
|
||||
'$ balena fleet MyFleet',
|
||||
'$ balena fleet myorg/myfleet',
|
||||
'$ balena fleet myorg/myfleet --view',
|
||||
];
|
||||
|
||||
public static args = [ca.fleetRequired];
|
||||
|
||||
public static usage = 'fleet <fleet>';
|
||||
|
||||
public static flags: flagsType.Input<FlagsDef> = {
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
view: flags.boolean({
|
||||
default: false,
|
||||
description: 'open fleet dashboard page',
|
||||
}),
|
||||
...cf.dataOutputFlags,
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
@ -72,9 +66,7 @@ export default class FleetCmd extends Command {
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const application = (await getApplication(balena, params.fleet, {
|
||||
const application = (await getApplication(getBalenaSdk(), params.fleet, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
@ -86,22 +78,26 @@ export default class FleetCmd extends Command {
|
||||
commit?: string;
|
||||
};
|
||||
|
||||
if (options.view) {
|
||||
const open = await import('open');
|
||||
const dashboardUrl = balena.models.application.getDashboardUrl(
|
||||
application.id,
|
||||
);
|
||||
await open(dashboardUrl, { wait: false });
|
||||
return;
|
||||
}
|
||||
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
application.commit = application.should_be_running__release[0]?.commit;
|
||||
|
||||
await this.outputData(
|
||||
application,
|
||||
['app_name', 'id', 'device_type', 'slug', 'commit'],
|
||||
options,
|
||||
);
|
||||
if (isV14()) {
|
||||
await this.outputData(
|
||||
application,
|
||||
['app_name', 'id', 'device_type', 'slug', 'commit'],
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
||||
console.log(`== ${application.slug}`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(application, [
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,100 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { getExpandedProp } from '../../utils/pine';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
slug: string;
|
||||
releaseToPinTo?: string;
|
||||
}
|
||||
|
||||
export default class FleetPinCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Pin a fleet to a release.
|
||||
|
||||
Pin a fleet to a release.
|
||||
|
||||
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena fleet pin myfleet',
|
||||
'$ balena fleet pin myorg/myfleet 91165e5',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'slug',
|
||||
description: 'the slug of the fleet to pin to a release',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseToPinTo',
|
||||
description: 'the commit of the release for the fleet to get pinned to',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'fleet pin <slug> [releaseToPinTo]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPinCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const fleet = await balena.models.application.get(params.slug, {
|
||||
$expand: {
|
||||
should_be_running__release: {
|
||||
$select: 'commit',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pinnedRelease = getExpandedProp(
|
||||
fleet.should_be_running__release,
|
||||
'commit',
|
||||
);
|
||||
|
||||
const releaseToPinTo = params.releaseToPinTo;
|
||||
const slug = params.slug;
|
||||
|
||||
if (!releaseToPinTo) {
|
||||
console.log(
|
||||
`${
|
||||
pinnedRelease
|
||||
? `This fleet is currently pinned to ${pinnedRelease}.`
|
||||
: 'This fleet is not currently pinned to any release.'
|
||||
} \n\nTo see a list of all releases this fleet can be pinned to, run \`balena releases ${slug}\`.`,
|
||||
);
|
||||
} else {
|
||||
await balena.models.application.pinToRelease(slug, releaseToPinTo);
|
||||
}
|
||||
}
|
||||
}
|
@ -80,7 +80,7 @@ export default class FleetRenameCmd extends Command {
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: ['slug'],
|
||||
$select: ['is_legacy'],
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -92,7 +92,7 @@ export default class FleetRenameCmd extends Command {
|
||||
|
||||
// Check app supports renaming
|
||||
const appType = (application.application_type as ApplicationType[])?.[0];
|
||||
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
|
||||
if (appType.is_legacy) {
|
||||
throw new ExpectedError(
|
||||
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
|
||||
);
|
||||
|
@ -62,9 +62,9 @@ export default class FleetRestartCmd extends Command {
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Disambiguate application
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
await balena.models.application.restart(application.slug);
|
||||
await balena.models.application.restart(application.id);
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +79,6 @@ export default class FleetRmCmd extends Command {
|
||||
const application = await getApplication(balena, params.fleet);
|
||||
|
||||
// Remove
|
||||
await balena.models.application.remove(application.slug);
|
||||
await balena.models.application.remove(application.id);
|
||||
}
|
||||
}
|
||||
|
@ -1,66 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export default class FleetTrackLatestCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Make this fleet track the latest release.
|
||||
|
||||
Make this fleet track the latest release.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena fleet track-latest myorg/myfleet',
|
||||
'$ balena fleet track-latest myfleet',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'slug',
|
||||
description: 'the slug of the fleet to make track the latest release',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'fleet track-latest <slug>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetTrackLatestCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
await balena.models.application.trackLatestRelease(params.slug);
|
||||
}
|
||||
}
|
@ -19,7 +19,8 @@ import { flags } from '@oclif/command';
|
||||
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { isV14 } from '../utils/version';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
@ -48,7 +49,7 @@ export default class FleetsCmd extends Command {
|
||||
public static usage = 'fleets';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...cf.dataSetOutputFlags,
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -78,17 +79,30 @@ export default class FleetsCmd extends Command {
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
});
|
||||
|
||||
await this.outputData(
|
||||
applications,
|
||||
[
|
||||
'id',
|
||||
'app_name',
|
||||
'slug',
|
||||
'device_type',
|
||||
'device_count',
|
||||
'online_devices',
|
||||
],
|
||||
options,
|
||||
);
|
||||
if (isV14()) {
|
||||
await this.outputData(
|
||||
applications,
|
||||
[
|
||||
'id',
|
||||
'app_name',
|
||||
'slug',
|
||||
'device_type',
|
||||
'device_count',
|
||||
'online_devices',
|
||||
],
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
'app_name => NAME',
|
||||
'slug',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -20,10 +20,13 @@ import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -52,27 +55,52 @@ export default class KeyCmd extends Command {
|
||||
public static usage = 'key <id>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
KeyCmd,
|
||||
);
|
||||
|
||||
const key = await getBalenaSdk().models.key.get(params.id);
|
||||
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
};
|
||||
if (isV14()) {
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
public_key: key.public_key,
|
||||
};
|
||||
|
||||
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
|
||||
if (!options.json) {
|
||||
// Id is redundant, since user must have provided it in command call
|
||||
this.printTitle(displayKey.name);
|
||||
this.outputMessage(displayKey.public_key);
|
||||
} else {
|
||||
await this.outputData(
|
||||
displayKey,
|
||||
['id', 'name', 'public_key'],
|
||||
options,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Old output implementation
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
};
|
||||
|
||||
// Since the public key string is long, it might
|
||||
// wrap to lines below, causing the table layout to break.
|
||||
// See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key);
|
||||
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
|
||||
|
||||
// Since the public key string is long, it might
|
||||
// wrap to lines below, causing the table layout to break.
|
||||
// See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2022 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -35,13 +38,14 @@ export default class KeysCmd extends Command {
|
||||
public static usage = 'keys';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(KeysCmd);
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(KeysCmd);
|
||||
|
||||
const keys = await getBalenaSdk().models.key.getAll();
|
||||
|
||||
@ -50,6 +54,12 @@ export default class KeysCmd extends Command {
|
||||
return { id: k.id, name: k.title };
|
||||
});
|
||||
|
||||
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
|
||||
// Display
|
||||
if (isV14()) {
|
||||
await this.outputData(displayKeys, ['id', 'name'], options);
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2022 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -36,12 +39,13 @@ export default class OrgsCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(OrgsCmd);
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(OrgsCmd);
|
||||
|
||||
const { getOwnOrganizations } = await import('../utils/sdk');
|
||||
|
||||
@ -49,8 +53,13 @@ export default class OrgsCmd extends Command {
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk());
|
||||
|
||||
// Display
|
||||
console.log(
|
||||
getVisuals().table.horizontal(organizations, ['name', 'handle']),
|
||||
);
|
||||
if (isV14()) {
|
||||
await this.outputData(organizations, ['name', 'handle'], options);
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(
|
||||
getVisuals().table.horizontal(organizations, ['name', 'handle']),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,11 +23,7 @@ import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import {
|
||||
applicationIdInfo,
|
||||
devModeInfo,
|
||||
secureBootInfo,
|
||||
} from '../../utils/messages';
|
||||
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
|
||||
|
||||
const CONNECTIONS_FOLDER = '/system-connections';
|
||||
|
||||
@ -40,7 +36,6 @@ interface FlagsDef {
|
||||
'config-wifi-key'?: string;
|
||||
'config-wifi-ssid'?: string;
|
||||
dev?: boolean; // balenaOS development variant
|
||||
secureBoot?: boolean;
|
||||
device?: string; // device UUID
|
||||
'device-type'?: string;
|
||||
help?: void;
|
||||
@ -48,7 +43,6 @@ interface FlagsDef {
|
||||
'system-connection': string[];
|
||||
'initial-device-name'?: string;
|
||||
'provisioning-key-name'?: string;
|
||||
'provisioning-key-expiry-date'?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -58,14 +52,12 @@ interface ArgsDef {
|
||||
interface Answers {
|
||||
appUpdatePollInterval: number; // in minutes
|
||||
developmentMode?: boolean; // balenaOS development variant
|
||||
secureBoot?: boolean;
|
||||
deviceType: string; // e.g. "raspberrypi3"
|
||||
network: 'ethernet' | 'wifi';
|
||||
version: string; // e.g. "2.32.0+rev1"
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
provisioningKeyName?: string;
|
||||
provisioningKeyExpiryDate?: string;
|
||||
}
|
||||
|
||||
export default class OsConfigureCmd extends Command {
|
||||
@ -86,8 +78,6 @@ export default class OsConfigureCmd extends Command {
|
||||
|
||||
${devModeInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
${secureBootInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
The --system-connection (-c) option is used to inject NetworkManager connection
|
||||
profiles for additional network interfaces, such as cellular/GSM or additional
|
||||
WiFi or ethernet connections. This option may be passed multiple times in case there
|
||||
@ -131,7 +121,7 @@ export default class OsConfigureCmd extends Command {
|
||||
config: flags.string({
|
||||
description:
|
||||
'path to a pre-generated config.json file to be injected in the OS image',
|
||||
exclusive: ['provisioning-key-name', 'provisioning-key-expiry-date'],
|
||||
exclusive: ['provisioning-key-name'],
|
||||
}),
|
||||
'config-app-update-poll-interval': flags.integer({
|
||||
description:
|
||||
@ -148,15 +138,7 @@ export default class OsConfigureCmd extends Command {
|
||||
description: 'WiFi SSID (network name) (non-interactive configuration)',
|
||||
}),
|
||||
dev: cf.dev,
|
||||
secureBoot: cf.secureBoot,
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: [
|
||||
'fleet',
|
||||
'provisioning-key-name',
|
||||
'provisioning-key-expiry-date',
|
||||
],
|
||||
},
|
||||
device: { ...cf.device, exclusive: ['fleet', 'provisioning-key-name'] },
|
||||
'device-type': flags.string({
|
||||
description:
|
||||
'device type slug (e.g. "raspberrypi3") to override the fleet device type',
|
||||
@ -179,11 +161,6 @@ export default class OsConfigureCmd extends Command {
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
exclusive: ['config', 'device'],
|
||||
}),
|
||||
'provisioning-key-expiry-date': flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
exclusive: ['config', 'device'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -224,7 +201,7 @@ export default class OsConfigureCmd extends Command {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
})) as ApplicationWithDeviceType;
|
||||
await checkDeviceTypeCompatibility(options, app);
|
||||
await checkDeviceTypeCompatibility(balena, options, app);
|
||||
deviceTypeSlug =
|
||||
options['device-type'] || app.is_for__device_type[0].slug;
|
||||
}
|
||||
@ -247,15 +224,6 @@ export default class OsConfigureCmd extends Command {
|
||||
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
||||
await validateDevOptionAndWarn(options.dev, osVersion);
|
||||
|
||||
const { validateSecureBootOptionAndWarn } = await import(
|
||||
'../../utils/config'
|
||||
);
|
||||
await validateSecureBootOptionAndWarn(
|
||||
options.secureBoot,
|
||||
deviceTypeSlug,
|
||||
osVersion,
|
||||
);
|
||||
|
||||
const answers: Answers = await askQuestionsForDeviceType(
|
||||
deviceTypeManifest,
|
||||
options,
|
||||
@ -266,9 +234,7 @@ export default class OsConfigureCmd extends Command {
|
||||
}
|
||||
answers.version = osVersion;
|
||||
answers.developmentMode = options.dev;
|
||||
answers.secureBoot = options.secureBoot;
|
||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
||||
|
||||
if (_.isEmpty(configJson)) {
|
||||
if (device) {
|
||||
@ -380,17 +346,17 @@ async function getOsVersionFromImage(
|
||||
* @param app Balena SDK Application model object
|
||||
*/
|
||||
async function checkDeviceTypeCompatibility(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
options: FlagsDef,
|
||||
app: ApplicationWithDeviceType,
|
||||
) {
|
||||
if (options['device-type']) {
|
||||
const [appDeviceType, optionDeviceType] = await Promise.all([
|
||||
sdk.models.device.getManifestBySlug(app.is_for__device_type[0].slug),
|
||||
sdk.models.device.getManifestBySlug(options['device-type']),
|
||||
]);
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (
|
||||
!(await helpers.areDeviceTypesCompatible(
|
||||
app.is_for__device_type[0].slug,
|
||||
options['device-type'],
|
||||
))
|
||||
) {
|
||||
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
|
||||
throw new ExpectedError(
|
||||
`Device type ${options['device-type']} is incompatible with fleet ${options.fleet}`,
|
||||
);
|
||||
|
@ -53,11 +53,9 @@ export default class OsDownloadCmd extends Command {
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.101.7',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2022.7.0',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.90.0',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2021.10.2.prod',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest',
|
||||
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default',
|
||||
|
@ -42,9 +42,7 @@ export default class OsInitializeCmd extends Command {
|
||||
Initialize an os image for a device.
|
||||
|
||||
Initialize an os image for a device with a previously
|
||||
configured operating system image and flash the
|
||||
an external storage drive or the device's storage
|
||||
medium depending on the device type.
|
||||
configured operating system image.
|
||||
${INIT_WARNING_MESSAGE}
|
||||
`;
|
||||
|
||||
|
@ -187,7 +187,7 @@ Can be repeated to add multiple certificates.\
|
||||
: undefined;
|
||||
|
||||
const progressBars: {
|
||||
[key: string]: InstanceType<ReturnType<typeof getVisuals>['Progress']>;
|
||||
[key: string]: ReturnType<typeof getVisuals>['Progress'];
|
||||
} = {};
|
||||
|
||||
const progressHandler = function (event: {
|
||||
@ -201,7 +201,7 @@ Can be repeated to add multiple certificates.\
|
||||
};
|
||||
|
||||
const spinners: {
|
||||
[key: string]: InstanceType<ReturnType<typeof getVisuals>['Spinner']>;
|
||||
[key: string]: ReturnType<typeof getVisuals>['Spinner'];
|
||||
} = {};
|
||||
|
||||
const spinnerHandler = function (event: { name: string; action: string }) {
|
||||
@ -288,7 +288,7 @@ Can be repeated to add multiple certificates.\
|
||||
preloader.on('error', reject);
|
||||
resolve(
|
||||
this.prepareAndPreload(preloader, balena, {
|
||||
slug: fleetSlug,
|
||||
appId: fleetSlug,
|
||||
commit,
|
||||
pinDevice,
|
||||
}),
|
||||
@ -491,10 +491,10 @@ Would you like to disable automatic updates for this fleet now?\
|
||||
});
|
||||
}
|
||||
|
||||
async getAppWithReleases(balenaSdk: BalenaSDK, slug: string) {
|
||||
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string) {
|
||||
const { getApplication } = await import('../utils/sdk');
|
||||
|
||||
return (await getApplication(balenaSdk, slug, {
|
||||
return (await getApplication(balenaSdk, appId, {
|
||||
$expand: this.applicationExpandOptions,
|
||||
})) as Application & { should_be_running__release: [Release?] };
|
||||
}
|
||||
@ -503,15 +503,15 @@ Would you like to disable automatic updates for this fleet now?\
|
||||
preloader: Preloader,
|
||||
balenaSdk: BalenaSDK,
|
||||
options: {
|
||||
slug?: string;
|
||||
appId?: string;
|
||||
commit?: string;
|
||||
pinDevice: boolean;
|
||||
},
|
||||
) {
|
||||
await preloader.prepare();
|
||||
|
||||
const application = options.slug
|
||||
? await this.getAppWithReleases(balenaSdk, options.slug)
|
||||
const application = options.appId
|
||||
? await this.getAppWithReleases(balenaSdk, options.appId)
|
||||
: await this.selectApplication(preloader.config.deviceType);
|
||||
|
||||
let commit: string; // commit hash or the strings 'latest' or 'current'
|
||||
@ -523,7 +523,7 @@ Would you like to disable automatic updates for this fleet now?\
|
||||
if (this.isCurrentCommit(options.commit)) {
|
||||
if (!appCommit) {
|
||||
throw new Error(
|
||||
`Unexpected empty commit hash for fleet slug "${application.slug}"`,
|
||||
`Unexpected empty commit hash for fleet ID "${application.id}"`,
|
||||
);
|
||||
}
|
||||
// handle `--commit current` (and its `--commit latest` synonym)
|
||||
|
@ -22,7 +22,7 @@ import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import type { BalenaSDK } from 'balena-sdk';
|
||||
import { ExpectedError, instanceOf } from '../errors';
|
||||
import { RegistrySecrets } from '@balena/compose/dist/multibuild';
|
||||
import { RegistrySecrets } from 'resin-multibuild';
|
||||
import { lowercaseIfSlug } from '../utils/normalization';
|
||||
import {
|
||||
applyReleaseTagKeysAndValues,
|
||||
@ -55,7 +55,6 @@ interface FlagsDef {
|
||||
'multi-dockerignore': boolean;
|
||||
'release-tag'?: string[];
|
||||
draft: boolean;
|
||||
note?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -98,7 +97,6 @@ export default class PushCmd extends Command {
|
||||
'$ balena push myFleet',
|
||||
'$ balena push myFleet --source <source directory>',
|
||||
'$ balena push myFleet -s <source directory>',
|
||||
'$ balena push myFleet --source <source directory> --note "this is the note for this release"',
|
||||
'$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"',
|
||||
'$ balena push myorg/myfleet',
|
||||
'',
|
||||
@ -178,7 +176,7 @@ export default class PushCmd extends Command {
|
||||
description: stripIndent`
|
||||
Don't run a live session on this push. The filesystem will not be monitored,
|
||||
and changes will not be synchronized to any running containers. Note that both
|
||||
this flag and --detached are required to cause the process to end once the
|
||||
this flag and --detached and required to cause the process to end once the
|
||||
initial build has completed.`,
|
||||
default: false,
|
||||
}),
|
||||
@ -243,7 +241,6 @@ export default class PushCmd extends Command {
|
||||
as final by default unless this option is given.`,
|
||||
default: false,
|
||||
}),
|
||||
note: flags.string({ description: 'The notes for this release' }),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -357,9 +354,6 @@ export default class PushCmd extends Command {
|
||||
releaseTagKeys,
|
||||
releaseTagValues,
|
||||
);
|
||||
if (options.note) {
|
||||
await sdk.models.release.note(releaseId, options.note);
|
||||
}
|
||||
} else if (releaseTagKeys.length > 0) {
|
||||
throw new Error(stripIndent`
|
||||
A release ID could not be parsed out of the builder's output.
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -22,8 +22,11 @@ import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import jsyaml = require('js-yaml');
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
composition?: boolean;
|
||||
}
|
||||
@ -49,7 +52,9 @@ export default class ReleaseCmd extends Command {
|
||||
default: false,
|
||||
char: 'c',
|
||||
description: 'Return the release composition',
|
||||
exclusive: ['json', 'fields'],
|
||||
}),
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static args = [
|
||||
@ -68,29 +73,27 @@ export default class ReleaseCmd extends Command {
|
||||
ReleaseCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
if (options.composition) {
|
||||
await this.showComposition(params.commitOrId, balena);
|
||||
await this.showComposition(params.commitOrId);
|
||||
} else {
|
||||
await this.showReleaseInfo(params.commitOrId, balena);
|
||||
await this.showReleaseInfo(params.commitOrId, options);
|
||||
}
|
||||
}
|
||||
|
||||
async showComposition(
|
||||
commitOrId: string | number,
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
) {
|
||||
const release = await balena.models.release.get(commitOrId, {
|
||||
async showComposition(commitOrId: string | number) {
|
||||
const release = await getBalenaSdk().models.release.get(commitOrId, {
|
||||
$select: 'composition',
|
||||
});
|
||||
|
||||
console.log(jsyaml.dump(release.composition));
|
||||
if (isV14()) {
|
||||
this.outputMessage(jsyaml.dump(release.composition));
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(jsyaml.dump(release.composition));
|
||||
}
|
||||
}
|
||||
|
||||
async showReleaseInfo(
|
||||
commitOrId: string | number,
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
) {
|
||||
async showReleaseInfo(commitOrId: string | number, options: FlagsDef) {
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
'commit',
|
||||
@ -103,7 +106,7 @@ export default class ReleaseCmd extends Command {
|
||||
'end_timestamp',
|
||||
];
|
||||
|
||||
const release = await balena.models.release.get(commitOrId, {
|
||||
const release = await getBalenaSdk().models.release.get(commitOrId, {
|
||||
$select: fields,
|
||||
$expand: {
|
||||
release_tag: {
|
||||
@ -116,13 +119,28 @@ export default class ReleaseCmd extends Command {
|
||||
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
|
||||
.join('\n');
|
||||
|
||||
const _ = await import('lodash');
|
||||
const values = _.mapValues(
|
||||
release,
|
||||
(val) => val ?? 'N/a',
|
||||
) as Dictionary<string>;
|
||||
values['tags'] = tagStr;
|
||||
if (isV14()) {
|
||||
await this.outputData(
|
||||
{
|
||||
tags: tagStr,
|
||||
...release,
|
||||
},
|
||||
fields,
|
||||
{
|
||||
displayNullValuesAs: 'N/a',
|
||||
...options,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Old output implementation
|
||||
const _ = await import('lodash');
|
||||
const values = _.mapValues(
|
||||
release,
|
||||
(val) => val ?? 'N/a',
|
||||
) as Dictionary<string>;
|
||||
values['tags'] = tagStr;
|
||||
|
||||
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
|
||||
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,83 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
commitOrId: string | number;
|
||||
}
|
||||
|
||||
export default class ReleaseInvalidateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Invalidate a release.
|
||||
|
||||
Invalidate a release.
|
||||
|
||||
Invalid releases are not automatically deployed to devices tracking the latest
|
||||
release. For an invalid release to be deployed to a device, the device should be
|
||||
explicity pinned to that release.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena release invalidate a777f7345fe3d655c1c981aa642e5555',
|
||||
'$ balena release invalidate 1234567',
|
||||
];
|
||||
|
||||
public static usage = 'release invalidate <commitOrId>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'commitOrId',
|
||||
description: 'the commit or ID of the release to invalidate',
|
||||
required: true,
|
||||
parse: (commitOrId: string) => tryAsInteger(commitOrId),
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(
|
||||
ReleaseInvalidateCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const release = await balena.models.release.get(params.commitOrId, {
|
||||
$select: ['id', 'is_invalidated'],
|
||||
});
|
||||
|
||||
if (release.is_invalidated) {
|
||||
console.log(`Release ${params.commitOrId} is already invalidated!`);
|
||||
return;
|
||||
}
|
||||
|
||||
await balena.models.release.setIsInvalidated(release.id, true);
|
||||
console.log(`Release ${params.commitOrId} invalidated`);
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
commitOrId: string | number;
|
||||
}
|
||||
|
||||
export default class ReleaseValidateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Validate a release.
|
||||
|
||||
Validate a release.
|
||||
|
||||
Valid releases are automatically deployed to devices tracking the latest
|
||||
release if they are finalized.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena release validate a777f7345fe3d655c1c981aa642e5555',
|
||||
'$ balena release validate 1234567',
|
||||
];
|
||||
|
||||
public static usage = 'release validate <commitOrId>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'commitOrId',
|
||||
description: 'the commit or ID of the release to validate',
|
||||
required: true,
|
||||
parse: (commitOrId: string) => tryAsInteger(commitOrId),
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleaseValidateCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const release = await balena.models.release.get(params.commitOrId, {
|
||||
$select: ['id', 'is_invalidated'],
|
||||
});
|
||||
|
||||
if (!release.is_invalidated) {
|
||||
console.log(`Release ${params.commitOrId} is already validated!`);
|
||||
return;
|
||||
}
|
||||
|
||||
await balena.models.release.setIsInvalidated(release.id, false);
|
||||
console.log(`Release ${params.commitOrId} validated`);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,8 +21,11 @@ import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationNameNote } from '../utils/messages';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -43,6 +46,7 @@ export default class ReleasesCmd extends Command {
|
||||
public static usage = 'releases <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -57,7 +61,9 @@ export default class ReleasesCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ReleasesCmd,
|
||||
);
|
||||
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
@ -76,12 +82,20 @@ export default class ReleasesCmd extends Command {
|
||||
{ $select: fields },
|
||||
);
|
||||
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
if (isV14()) {
|
||||
await this.outputData(releases, fields, {
|
||||
displayNullValuesAs: 'N/a',
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { LocalBalenaOsDevice } from 'balena-sync';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getCliUx, stripIndent } from '../utils/lazy';
|
||||
@ -71,7 +72,7 @@ export default class ScanCmd extends Command {
|
||||
|
||||
public async run() {
|
||||
const _ = await import('lodash');
|
||||
const { discoverLocalBalenaOsDevices } = await import('../utils/discover');
|
||||
const { discover } = await import('balena-sync');
|
||||
const prettyjson = await import('prettyjson');
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
|
||||
@ -87,7 +88,8 @@ export default class ScanCmd extends Command {
|
||||
const ux = getCliUx();
|
||||
ux.action.start('Scanning for local balenaOS devices');
|
||||
|
||||
const localDevices = await discoverLocalBalenaOsDevices(discoverTimeout);
|
||||
const localDevices: LocalBalenaOsDevice[] =
|
||||
await discover.discoverLocalBalenaOsDevices(discoverTimeout);
|
||||
const engineReachableDevices: boolean[] = await Promise.all(
|
||||
localDevices.map(async ({ address }: { address: string }) => {
|
||||
const docker = await dockerUtils.createClient({
|
||||
@ -104,7 +106,7 @@ export default class ScanCmd extends Command {
|
||||
}),
|
||||
);
|
||||
|
||||
const developmentDevices = localDevices.filter(
|
||||
const developmentDevices: LocalBalenaOsDevice[] = localDevices.filter(
|
||||
(_localDevice, index) => engineReachableDevices[index],
|
||||
);
|
||||
|
||||
@ -114,15 +116,18 @@ export default class ScanCmd extends Command {
|
||||
_.isEqual,
|
||||
);
|
||||
|
||||
const productionDevicesInfo = productionDevices.map((device) => {
|
||||
return {
|
||||
host: device.host,
|
||||
address: device.address,
|
||||
osVariant: 'production',
|
||||
dockerInfo: undefined,
|
||||
dockerVersion: undefined,
|
||||
};
|
||||
});
|
||||
const productionDevicesInfo = _.map(
|
||||
productionDevices,
|
||||
(device: LocalBalenaOsDevice) => {
|
||||
return {
|
||||
host: device.host,
|
||||
address: device.address,
|
||||
osVariant: 'production',
|
||||
dockerInfo: undefined,
|
||||
dockerVersion: undefined,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Query devices for info
|
||||
const devicesInfo = await Promise.all(
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import type { DataOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -35,15 +38,27 @@ export default class SettingsCmd extends Command {
|
||||
public static usage = 'settings';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV14() ? cf.dataOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(SettingsCmd);
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(SettingsCmd);
|
||||
|
||||
const settings = await getBalenaSdk().settings.getAll();
|
||||
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(settings));
|
||||
if (isV14()) {
|
||||
// Select all available fields for display
|
||||
const fields = Object.keys(settings);
|
||||
|
||||
await this.outputData(settings, fields, {
|
||||
noCapitalizeKeys: true,
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
// Old output implementation
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(settings));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { parseAsInteger, validateLocalHostnameOrIp } from '../utils/validation';
|
||||
import * as BalenaSdk from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
port?: number;
|
||||
@ -76,7 +77,8 @@ export default class SshCmd extends Command {
|
||||
public static args = [
|
||||
{
|
||||
name: 'fleetOrDevice',
|
||||
description: 'fleet name/slug, device uuid, or address of local device',
|
||||
description:
|
||||
'fleet name/slug/id, device uuid, or address of local device',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
@ -126,8 +128,8 @@ export default class SshCmd extends Command {
|
||||
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
|
||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||
return await performLocalDeviceSSH({
|
||||
hostname: params.fleetOrDevice,
|
||||
port: options.port || 'local',
|
||||
address: params.fleetOrDevice,
|
||||
port: options.port,
|
||||
forceTTY: options.tty,
|
||||
verbose: options.verbose,
|
||||
service: params.service,
|
||||
@ -150,6 +152,12 @@ export default class SshCmd extends Command {
|
||||
params.fleetOrDevice,
|
||||
);
|
||||
|
||||
const device = await sdk.models.device.get(deviceUuid, {
|
||||
$select: ['id', 'supervisor_version', 'is_online'],
|
||||
});
|
||||
|
||||
const deviceId = device.id;
|
||||
const supervisorVersion = device.supervisor_version;
|
||||
const { which } = await import('../utils/which');
|
||||
|
||||
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
|
||||
@ -201,15 +209,19 @@ export default class SshCmd extends Command {
|
||||
// that we know exists and is accessible
|
||||
let containerId: string | undefined;
|
||||
if (params.service != null) {
|
||||
const { getContainerIdForService } = await import('../utils/device/ssh');
|
||||
containerId = await getContainerIdForService({
|
||||
containerId = await this.getContainerId(
|
||||
sdk,
|
||||
deviceUuid,
|
||||
hostname: `ssh.${proxyUrl}`,
|
||||
port: options.port || 'cloud',
|
||||
proxyCommand,
|
||||
service: params.service,
|
||||
username: username!,
|
||||
});
|
||||
params.service,
|
||||
{
|
||||
port: options.port,
|
||||
proxyCommand,
|
||||
proxyUrl: proxyUrl || '',
|
||||
username: username!,
|
||||
},
|
||||
supervisorVersion,
|
||||
deviceId,
|
||||
);
|
||||
}
|
||||
|
||||
let accessCommand: string;
|
||||
@ -218,14 +230,158 @@ export default class SshCmd extends Command {
|
||||
} else {
|
||||
accessCommand = `host ${deviceUuid}`;
|
||||
}
|
||||
const { runRemoteCommand } = await import('../utils/ssh');
|
||||
await runRemoteCommand({
|
||||
cmd: accessCommand,
|
||||
hostname: `ssh.${proxyUrl}`,
|
||||
port: options.port || 'cloud',
|
||||
proxyCommand,
|
||||
username,
|
||||
|
||||
const command = this.generateVpnSshCommand({
|
||||
uuid: deviceUuid,
|
||||
command: accessCommand,
|
||||
verbose: options.verbose,
|
||||
port: options.port,
|
||||
proxyCommand,
|
||||
proxyUrl: proxyUrl || '',
|
||||
username: username!,
|
||||
});
|
||||
|
||||
const { spawnSshAndThrowOnError } = await import('../utils/ssh');
|
||||
return spawnSshAndThrowOnError(command);
|
||||
}
|
||||
|
||||
async getContainerId(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
uuid: string,
|
||||
serviceName: string,
|
||||
sshOpts: {
|
||||
port?: number;
|
||||
proxyCommand?: string[];
|
||||
proxyUrl: string;
|
||||
username: string;
|
||||
},
|
||||
version?: string,
|
||||
id?: number,
|
||||
): Promise<string> {
|
||||
const semver = await import('balena-semver');
|
||||
|
||||
if (version == null || id == null) {
|
||||
const device = await sdk.models.device.get(uuid, {
|
||||
$select: ['id', 'supervisor_version'],
|
||||
});
|
||||
version = device.supervisor_version;
|
||||
id = device.id;
|
||||
}
|
||||
|
||||
let containerId: string | undefined;
|
||||
if (semver.gte(version, '8.6.0')) {
|
||||
const apiUrl = await sdk.settings.get('apiUrl');
|
||||
// TODO: Move this into the SDKs device model
|
||||
const request = await sdk.request.send({
|
||||
method: 'POST',
|
||||
url: '/supervisor/v2/containerId',
|
||||
baseUrl: apiUrl,
|
||||
body: {
|
||||
method: 'GET',
|
||||
deviceId: id,
|
||||
},
|
||||
});
|
||||
if (request.status !== 200) {
|
||||
throw new Error(
|
||||
`There was an error connecting to device ${uuid}, HTTP response code: ${request.status}.`,
|
||||
);
|
||||
}
|
||||
const body = request.body;
|
||||
if (body.status !== 'success') {
|
||||
throw new Error(
|
||||
`There was an error communicating with device ${uuid}.\n\tError: ${body.message}`,
|
||||
);
|
||||
}
|
||||
containerId = body.services[serviceName];
|
||||
} else {
|
||||
console.error(stripIndent`
|
||||
Using legacy method to detect container ID. This will be slow.
|
||||
To speed up this process, please update your device to an OS
|
||||
which has a supervisor version of at least v8.6.0.
|
||||
`);
|
||||
// We need to execute a balena ps command on the device,
|
||||
// and parse the output, looking for a specific
|
||||
// container
|
||||
const childProcess = await import('child_process');
|
||||
const { escapeRegExp } = await import('lodash');
|
||||
const { which } = await import('../utils/which');
|
||||
const { deviceContainerEngineBinary } = await import(
|
||||
'../utils/device/ssh'
|
||||
);
|
||||
|
||||
const sshBinary = await which('ssh');
|
||||
const sshArgs = this.generateVpnSshCommand({
|
||||
uuid,
|
||||
verbose: false,
|
||||
port: sshOpts.port,
|
||||
command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`,
|
||||
proxyCommand: sshOpts.proxyCommand,
|
||||
proxyUrl: sshOpts.proxyUrl,
|
||||
username: sshOpts.username,
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`);
|
||||
}
|
||||
const subProcess = childProcess.spawn(sshBinary, sshArgs, {
|
||||
stdio: [null, 'pipe', null],
|
||||
});
|
||||
const containers = await new Promise<string>((resolve, reject) => {
|
||||
const output: string[] = [];
|
||||
subProcess.stdout.on('data', (chunk) => output.push(chunk.toString()));
|
||||
subProcess.on('close', (code: number) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`Non-zero error code when looking for service container: ${code}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resolve(output.join(''));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const lines = containers.split('\n');
|
||||
const regex = new RegExp(`\\/?${escapeRegExp(serviceName)}_\\d+_\\d+`);
|
||||
for (const container of lines) {
|
||||
const [cId, name] = container.split(' ');
|
||||
if (regex.test(name)) {
|
||||
containerId = cId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (containerId == null) {
|
||||
throw new Error(
|
||||
`Could not find a service ${serviceName} on device ${uuid}.`,
|
||||
);
|
||||
}
|
||||
return containerId;
|
||||
}
|
||||
|
||||
generateVpnSshCommand(opts: {
|
||||
uuid: string;
|
||||
command: string;
|
||||
verbose: boolean;
|
||||
port?: number;
|
||||
username: string;
|
||||
proxyUrl: string;
|
||||
proxyCommand?: string[];
|
||||
}) {
|
||||
return [
|
||||
...(opts.verbose ? ['-vvv'] : []),
|
||||
'-t',
|
||||
...['-o', 'LogLevel=ERROR'],
|
||||
...['-o', 'StrictHostKeyChecking=no'],
|
||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||
...(opts.proxyCommand && opts.proxyCommand.length
|
||||
? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`]
|
||||
: []),
|
||||
...(opts.port ? ['-p', opts.port.toString()] : []),
|
||||
`${opts.username}@ssh.${opts.proxyUrl}`,
|
||||
opts.command,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ export default class SupportCmd extends Command {
|
||||
console.log(
|
||||
`Access has been granted for ${duration}, expiring ${new Date(
|
||||
expiryTs,
|
||||
).toISOString()}`,
|
||||
).toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,8 @@ export default class TagRmCmd extends Command {
|
||||
throw new ExpectedError(TagRmCmd.missingResourceMessage);
|
||||
}
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
|
||||
if (options.fleet) {
|
||||
const { getFleetSlug } = await import('../../utils/sdk');
|
||||
return balena.models.application.tags.remove(
|
||||
@ -98,7 +100,10 @@ export default class TagRmCmd extends Command {
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
return balena.models.device.tags.remove(options.device, params.tagKey);
|
||||
return balena.models.device.tags.remove(
|
||||
tryAsInteger(options.device),
|
||||
params.tagKey,
|
||||
);
|
||||
}
|
||||
if (options.release) {
|
||||
const { disambiguateReleaseParam } = await import(
|
||||
|
@ -105,6 +105,8 @@ export default class TagSetCmd extends Command {
|
||||
|
||||
params.value ??= '';
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
|
||||
if (options.fleet) {
|
||||
const { getFleetSlug } = await import('../../utils/sdk');
|
||||
return balena.models.application.tags.set(
|
||||
@ -115,7 +117,7 @@ export default class TagSetCmd extends Command {
|
||||
}
|
||||
if (options.device) {
|
||||
return balena.models.device.tags.set(
|
||||
options.device,
|
||||
tryAsInteger(options.device),
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,8 +21,12 @@ import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationIdInfo } from '../utils/messages';
|
||||
import type { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
import { isV14 } from '../utils/version';
|
||||
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
fleet?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
@ -61,6 +65,7 @@ export default class TagsCmd extends Command {
|
||||
...cf.release,
|
||||
exclusive: ['fleet', 'device'],
|
||||
},
|
||||
...(isV14() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -76,7 +81,9 @@ export default class TagsCmd extends Command {
|
||||
throw new ExpectedError(this.missingResourceMessage);
|
||||
}
|
||||
|
||||
let tags;
|
||||
const { tryAsInteger } = await import('../utils/validation');
|
||||
|
||||
let tags: ApplicationTag[] | DeviceTag[] | ReleaseTag[] = [];
|
||||
|
||||
if (options.fleet) {
|
||||
const { getFleetSlug } = await import('../utils/sdk');
|
||||
@ -85,7 +92,9 @@ export default class TagsCmd extends Command {
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
tags = await balena.models.device.tags.getAllByDevice(options.device);
|
||||
tags = await balena.models.device.tags.getAllByDevice(
|
||||
tryAsInteger(options.device),
|
||||
);
|
||||
}
|
||||
if (options.release) {
|
||||
const { disambiguateReleaseParam } = await import(
|
||||
@ -99,11 +108,17 @@ export default class TagsCmd extends Command {
|
||||
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
|
||||
}
|
||||
|
||||
if (!tags || tags.length === 0) {
|
||||
if (tags.length === 0 && !options.json) {
|
||||
// TODO: Later change to output message
|
||||
throw new ExpectedError('No tags found');
|
||||
}
|
||||
|
||||
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
|
||||
if (isV14()) {
|
||||
await this.outputData(tags, ['tag_key', 'value'], options);
|
||||
} else {
|
||||
// Old output implementation
|
||||
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
|
||||
}
|
||||
}
|
||||
|
||||
protected missingResourceMessage = stripIndent`
|
||||
|
@ -82,7 +82,7 @@ export default class TunnelCmd extends Command {
|
||||
public static args = [
|
||||
{
|
||||
name: 'deviceOrFleet',
|
||||
description: 'device UUID or fleet name/slug',
|
||||
description: 'device UUID or fleet name/slug/ID',
|
||||
required: true,
|
||||
parse: lowercaseIfSlug,
|
||||
},
|
||||
@ -136,7 +136,8 @@ export default class TunnelCmd extends Command {
|
||||
// Ascertain device uuid
|
||||
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
|
||||
const uuid = await getOnlineTargetDeviceUuid(sdk, params.deviceOrFleet);
|
||||
logger.logInfo(`Opening a tunnel to ${uuid}...`);
|
||||
const device = await sdk.models.device.get(uuid);
|
||||
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
|
||||
|
||||
const _ = await import('lodash');
|
||||
const localListeners = _.chain(options.port)
|
||||
@ -146,27 +147,31 @@ export default class TunnelCmd extends Command {
|
||||
.map(async ({ localPort, localAddress, remotePort }) => {
|
||||
try {
|
||||
const { tunnelConnectionToDevice } = await import('../utils/tunnel');
|
||||
const handler = await tunnelConnectionToDevice(uuid, remotePort, sdk);
|
||||
const handler = await tunnelConnectionToDevice(
|
||||
device.uuid,
|
||||
remotePort,
|
||||
sdk,
|
||||
);
|
||||
|
||||
const { createServer } = await import('net');
|
||||
const server = createServer(async (client: Socket) => {
|
||||
try {
|
||||
await handler(client);
|
||||
logConnection(
|
||||
client.remoteAddress ?? '',
|
||||
client.remotePort ?? 0,
|
||||
client.localAddress ?? '',
|
||||
client.localPort ?? 0,
|
||||
uuid,
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
);
|
||||
} catch (err) {
|
||||
logConnection(
|
||||
client.remoteAddress ?? '',
|
||||
client.remotePort ?? 0,
|
||||
client.localAddress ?? '',
|
||||
client.localPort ?? 0,
|
||||
uuid,
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
err,
|
||||
);
|
||||
@ -181,15 +186,15 @@ export default class TunnelCmd extends Command {
|
||||
});
|
||||
|
||||
logger.logInfo(
|
||||
` - tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}`,
|
||||
` - tunnelling ${localAddress}:${localPort} to ${device.uuid}:${remotePort}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.logWarn(
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}, failed ${JSON.stringify(
|
||||
err.message,
|
||||
)}`,
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
|
||||
);
|
||||
|
||||
return false;
|
||||
|
@ -86,7 +86,7 @@ export class DeprecationChecker {
|
||||
* @param version Semver without 'v' prefix, e.g. '12.0.0.'
|
||||
*/
|
||||
protected getNpmUrl(version: string) {
|
||||
return `https://registry.npmjs.org/balena-cli/${version}`;
|
||||
return `http://registry.npmjs.org/balena-cli/${version}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -177,16 +177,7 @@ const messages: {
|
||||
Looks like the session token has expired.
|
||||
Try logging in again with the "balena login" command.`,
|
||||
|
||||
BalenaInvalidDeviceType: (
|
||||
error: Error & { deviceTypeSlug?: string; type?: string },
|
||||
) => {
|
||||
// TODO: The SDK should be throwing a different Error for this case.
|
||||
if (
|
||||
typeof error.type === 'string' &&
|
||||
error.type.startsWith('Incompatible ')
|
||||
) {
|
||||
return error.type;
|
||||
}
|
||||
BalenaInvalidDeviceType: (error: Error & { deviceTypeSlug?: string }) => {
|
||||
const slug = error.deviceTypeSlug ? `"${error.deviceTypeSlug}"` : 'slug';
|
||||
return stripIndent`
|
||||
Device type ${slug} not recognized. Perhaps misspelled?
|
||||
|
103
lib/events.ts
103
lib/events.ts
@ -16,7 +16,12 @@
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import { getBalenaSdk, stripIndent } from './utils/lazy';
|
||||
|
||||
interface CachedUsername {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track balena CLI usage events (product improvement analytics).
|
||||
@ -24,7 +29,7 @@ import { stripIndent } from './utils/lazy';
|
||||
* @param commandSignature A string like, for example:
|
||||
* "push <fleetOrDevice>"
|
||||
* That's literally so: "fleetOrDevice" is NOT replaced with the actual
|
||||
* fleet slug or device uuid. The purpose is to find out the most / least
|
||||
* fleet ID or device ID. The purpose is to find out the most / least
|
||||
* used command verbs, so we can focus our development effort where it is most
|
||||
* beneficial to end users.
|
||||
*
|
||||
@ -44,13 +49,40 @@ export async function trackCommand(commandSignature: string) {
|
||||
scope.setExtra('command', commandSignature);
|
||||
});
|
||||
}
|
||||
const { getCachedUsername } = await import('./utils/bootstrap');
|
||||
let username: string | undefined;
|
||||
try {
|
||||
username = (await getCachedUsername())?.username;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const settings = await import('balena-settings-client');
|
||||
|
||||
const username = await (async () => {
|
||||
const getStorage = await import('balena-settings-storage');
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
const storage = getStorage({ dataDirectory });
|
||||
let token;
|
||||
try {
|
||||
token = await storage.get('token');
|
||||
} catch {
|
||||
// If we can't get a token then we can't get a username
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = (await storage.get('cachedUsername')) as CachedUsername;
|
||||
if (result.token === token) {
|
||||
return result.username;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const balena = getBalenaSdk();
|
||||
const $username = await balena.auth.whoami();
|
||||
await storage.set('cachedUsername', {
|
||||
token,
|
||||
username: $username,
|
||||
} as CachedUsername);
|
||||
return $username;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry!.configureScope((scope) => {
|
||||
scope.setUser({
|
||||
@ -64,7 +96,6 @@ export async function trackCommand(commandSignature: string) {
|
||||
!process.env.BALENA_CLI_TEST_TYPE &&
|
||||
!process.env.BALENARC_NO_ANALYTICS
|
||||
) {
|
||||
const settings = await import('balena-settings-client');
|
||||
const balenaUrl = settings.get<string>('balenaUrl');
|
||||
await sendEvent(balenaUrl, `[CLI] ${commandSignature}`, username);
|
||||
}
|
||||
@ -73,52 +104,38 @@ export async function trackCommand(commandSignature: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const TIMEOUT = 4000;
|
||||
|
||||
/**
|
||||
* Make the event tracking HTTPS request to balenaCloud's '/mixpanel' endpoint.
|
||||
*/
|
||||
async function sendEvent(balenaUrl: string, event: string, username?: string) {
|
||||
const { default: got } = await import('got');
|
||||
const trackData = {
|
||||
api_key: 'balena-main',
|
||||
events: [
|
||||
{
|
||||
event_type: event,
|
||||
user_id: username,
|
||||
version_name: packageJSON.version,
|
||||
event_properties: {
|
||||
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
|
||||
arch: process.arch,
|
||||
platform: process.platform,
|
||||
node: process.version,
|
||||
},
|
||||
},
|
||||
],
|
||||
event,
|
||||
properties: {
|
||||
arch: process.arch,
|
||||
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
|
||||
distinct_id: username,
|
||||
mp_lib: 'node',
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
token: 'balena-main',
|
||||
version: packageJSON.version,
|
||||
},
|
||||
};
|
||||
const url = `https://api.${balenaUrl}/mixpanel/track`;
|
||||
const searchParams = {
|
||||
ip: 0,
|
||||
verbose: 0,
|
||||
data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
|
||||
};
|
||||
const url = `https://data.${balenaUrl}/amplitude/2/httpapi`;
|
||||
|
||||
try {
|
||||
await got.post(url, {
|
||||
json: trackData,
|
||||
retry: 0,
|
||||
timeout: {
|
||||
// Starts when the request is initiated.
|
||||
request: TIMEOUT,
|
||||
// Starts when request has been flushed.
|
||||
// Exits the request as soon as it's sent.
|
||||
response: 0,
|
||||
},
|
||||
});
|
||||
await got(url, { searchParams, retry: 0, timeout: 4000 });
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Event tracking error: ${e.message || e}`);
|
||||
}
|
||||
|
||||
if (
|
||||
e instanceof got.TimeoutError &&
|
||||
TIMEOUT < (e.timings.phases.total ?? 0)
|
||||
) {
|
||||
if (e instanceof got.TimeoutError) {
|
||||
console.error(stripIndent`
|
||||
Timeout submitting analytics event to balenaCloud/openBalena.
|
||||
If you are using the balena CLI in an air-gapped environment with a filtered
|
||||
|
@ -1,26 +1,35 @@
|
||||
/*
|
||||
Copyright 2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getCliUx, getChalk } from '../utils/lazy';
|
||||
|
||||
/**
|
||||
* Used to extend FlagsDef for commands that output single-record data.
|
||||
* Exposed to user in command options.
|
||||
*/
|
||||
export interface DataOutputOptions {
|
||||
fields?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to extend FlagsDef for commands that output multi-record data.
|
||||
* Exposed to user in command options.
|
||||
*/
|
||||
export interface DataSetOutputOptions extends DataOutputOptions {
|
||||
filter?: string;
|
||||
'no-header'?: boolean;
|
||||
@ -28,6 +37,14 @@ export interface DataSetOutputOptions extends DataOutputOptions {
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
// Not exposed to user
|
||||
export interface InternalOutputOptions {
|
||||
displayNullValuesAs?: string;
|
||||
hideNullOrUndefinedValues?: boolean;
|
||||
titleField?: string;
|
||||
noCapitalizeKeys?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output message to STDERR
|
||||
*/
|
||||
@ -49,7 +66,7 @@ export function outputMessage(msg: string) {
|
||||
export async function outputData(
|
||||
data: any[] | {},
|
||||
fields: string[],
|
||||
options: DataOutputOptions | DataSetOutputOptions,
|
||||
options: (DataOutputOptions | DataSetOutputOptions) & InternalOutputOptions,
|
||||
) {
|
||||
if (Array.isArray(data)) {
|
||||
await outputDataSet(data, fields, options as DataSetOutputOptions);
|
||||
@ -68,7 +85,7 @@ export async function outputData(
|
||||
async function outputDataSet(
|
||||
data: any[],
|
||||
fields: string[],
|
||||
options: DataSetOutputOptions,
|
||||
options: DataSetOutputOptions & InternalOutputOptions,
|
||||
) {
|
||||
// Oclif expects fields to be specified in the format used in table headers (though lowercase)
|
||||
// By replacing underscores with spaces here, we can support both header format and actual field name
|
||||
@ -77,6 +94,12 @@ async function outputDataSet(
|
||||
options.filter = options.filter?.replace(/_/g, ' ');
|
||||
options.sort = options.sort?.replace(/_/g, ' ');
|
||||
|
||||
if (!options.json) {
|
||||
data = data.map((d) => {
|
||||
return processNullValues(d, options);
|
||||
});
|
||||
}
|
||||
|
||||
getCliUx().table(
|
||||
data,
|
||||
// Convert fields array to column object keys
|
||||
@ -97,7 +120,7 @@ async function outputDataSet(
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a single data object (like `resin-cli-visuals table.vertical`),
|
||||
* Outputs a single data object (similar to `resin-cli-visuals table.vertical`),
|
||||
* but supporting a subset of options from `cli-ux table` (--json and --fields)
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
@ -107,9 +130,9 @@ async function outputDataSet(
|
||||
async function outputDataItem(
|
||||
data: any,
|
||||
fields: string[],
|
||||
options: DataOutputOptions,
|
||||
options: DataOutputOptions & InternalOutputOptions,
|
||||
) {
|
||||
const outData: typeof data = {};
|
||||
let outData: typeof data = {};
|
||||
|
||||
// Convert comma separated list of fields in `options.fields` to array of correct format.
|
||||
// Note, user may have specified the true field name (e.g. `some_field`),
|
||||
@ -125,30 +148,83 @@ async function outputDataItem(
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
(options.displayNullValuesAs || options.hideNullOrUndefinedValues) &&
|
||||
!options.json
|
||||
) {
|
||||
outData = processNullValues(outData, options);
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
printLine(JSON.stringify(outData, undefined, 2));
|
||||
} else {
|
||||
const chalk = getChalk();
|
||||
const { capitalize } = await import('lodash');
|
||||
|
||||
// Find longest key, so we can align results
|
||||
const longestKeyLength = getLongestObjectKeyLength(outData);
|
||||
|
||||
if (options.titleField) {
|
||||
printTitle(data[options.titleField as keyof any[]], options);
|
||||
}
|
||||
|
||||
// Output one field per line
|
||||
for (const [k, v] of Object.entries(outData)) {
|
||||
for (let [k, v] of Object.entries(outData)) {
|
||||
const shim = ' '.repeat(longestKeyLength - k.length);
|
||||
const kDisplay = capitalize(k.replace(/_/g, ' '));
|
||||
printLine(`${chalk.bold(kDisplay) + shim} : ${v}`);
|
||||
let kDisplay = k.replace(/_/g, ' ');
|
||||
|
||||
// Start multiline values on the line below the field name
|
||||
if (typeof v === 'string' && v.includes('\n')) {
|
||||
v = `\n${v}`;
|
||||
}
|
||||
|
||||
if (!options.noCapitalizeKeys) {
|
||||
kDisplay = capitalize(kDisplay);
|
||||
}
|
||||
if (k !== options.titleField) {
|
||||
printLine(` ${bold(kDisplay) + shim} : ${v}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLongestObjectKeyLength(o: any): number {
|
||||
return Object.keys(o).length >= 1
|
||||
? Object.keys(o).reduce((a, b) => {
|
||||
return a.length > b.length ? a : b;
|
||||
}).length
|
||||
: 0;
|
||||
/**
|
||||
* Amend null/undefined values in data as per options:
|
||||
* - options.displayNullValuesAs will replace the value with the specified string
|
||||
* - options.hideNullOrUndefinedValues will remove the property from the data
|
||||
*
|
||||
* @param data The data object to process
|
||||
* @param options Output options
|
||||
*
|
||||
* @returns a copy of the data with amended values.
|
||||
*/
|
||||
function processNullValues(data: any, options: InternalOutputOptions) {
|
||||
const dataCopy = { ...data };
|
||||
|
||||
Object.entries(dataCopy).forEach(([k, v]) => {
|
||||
if (v == null) {
|
||||
if (options.displayNullValuesAs) {
|
||||
dataCopy[k] = options.displayNullValuesAs;
|
||||
} else if (options.hideNullOrUndefinedValues) {
|
||||
delete dataCopy[k];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return dataCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a title with underscore
|
||||
*
|
||||
* @param title The title string to print
|
||||
* @param options Output options
|
||||
*/
|
||||
export function printTitle(
|
||||
title: string,
|
||||
options?: InternalOutputOptions & DataSetOutputOptions,
|
||||
) {
|
||||
if (!options?.['no-header']) {
|
||||
printLine(` ${capitalize(bold(title))}`);
|
||||
printLine(` ${bold('─'.repeat(title.length))}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printLine(s: any) {
|
||||
@ -156,3 +232,15 @@ function printLine(s: any) {
|
||||
// but using this one explicitly for ease of testing
|
||||
process.stdout.write(s + '\n');
|
||||
}
|
||||
|
||||
function capitalize(s: string) {
|
||||
return `${s[0].toUpperCase()}${s.slice(1)}`;
|
||||
}
|
||||
|
||||
function bold(s: string) {
|
||||
return getChalk().bold(s);
|
||||
}
|
||||
|
||||
function getLongestObjectKeyLength(o: any): number {
|
||||
return Math.max(0, ...Object.keys(o).map((k) => k.length));
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ export default class BalenaHelp extends Help {
|
||||
.map((pc) => {
|
||||
return commands.find((c) => c.id === pc.replace(' ', ':'));
|
||||
})
|
||||
.filter((c): c is (typeof commands)[0] => !!c);
|
||||
.filter((c): c is typeof commands[0] => !!c);
|
||||
|
||||
let usageLength = 0;
|
||||
for (const cmd of primaryCommands) {
|
||||
|
@ -53,6 +53,12 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
||||
if (extractBooleanFlag(cmdSlice, '--debug')) {
|
||||
process.env.DEBUG = '1';
|
||||
}
|
||||
// support global --v-next flag
|
||||
if (extractBooleanFlag(cmdSlice, '--v-next')) {
|
||||
const { version } = await import('../package.json');
|
||||
const { inc } = await import('semver');
|
||||
process.env.BALENA_CLI_VERSION_OVERRIDE = inc(version, 'major') || '';
|
||||
}
|
||||
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
|
||||
}
|
||||
|
||||
|
@ -119,61 +119,3 @@ export async function pkgExec(modFunc: string, args: string[]) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CachedUsername {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
let cachedUsername: CachedUsername | undefined;
|
||||
|
||||
/**
|
||||
* Return the parsed contents of the `~/.balena/cachedUsername` file. If the file
|
||||
* does not exist, create it with the details from the cloud. If not connected
|
||||
* to the internet, return undefined. This function is used by `lib/events.ts`
|
||||
* (event tracking) and `lib/utils/device/ssh.ts` and needs to gracefully handle
|
||||
* the scenario of not being connected to the internet.
|
||||
*/
|
||||
export async function getCachedUsername(): Promise<CachedUsername | undefined> {
|
||||
if (cachedUsername) {
|
||||
return cachedUsername;
|
||||
}
|
||||
const [{ getBalenaSdk }, getStorage, settings] = await Promise.all([
|
||||
import('./lazy'),
|
||||
import('balena-settings-storage'),
|
||||
import('balena-settings-client'),
|
||||
]);
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
const storage = getStorage({ dataDirectory });
|
||||
let token: string | undefined;
|
||||
try {
|
||||
token = (await storage.get('token')) as string | undefined;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (!token) {
|
||||
// If we can't get a token then we can't get a username
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = (await storage.get('cachedUsername')) as
|
||||
| CachedUsername
|
||||
| undefined;
|
||||
if (result && result.token === token && result.username) {
|
||||
cachedUsername = result;
|
||||
return cachedUsername;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const username = await getBalenaSdk().auth.whoami();
|
||||
if (username) {
|
||||
cachedUsername = { token, username };
|
||||
await storage.set('cachedUsername', cachedUsername);
|
||||
}
|
||||
} catch {
|
||||
// ignore (not connected to the internet?)
|
||||
}
|
||||
return cachedUsername;
|
||||
}
|
||||
|
@ -107,6 +107,16 @@ export const getDeviceAndMaybeAppFromUUID = _.memoize(
|
||||
(_sdk, deviceUUID) => deviceUUID,
|
||||
);
|
||||
|
||||
/** Given a device type alias like 'nuc', return the actual slug like 'intel-nuc'. */
|
||||
export const unaliasDeviceType = _.memoize(async function (
|
||||
sdk: SDK.BalenaSDK,
|
||||
deviceType: string,
|
||||
): Promise<string> {
|
||||
return (
|
||||
(await sdk.models.device.getManifestBySlug(deviceType)).slug || deviceType
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Download balenaOS image for the specified `deviceType`.
|
||||
* `OSVersion` may be one of:
|
||||
@ -245,8 +255,8 @@ export async function getOsVersions(
|
||||
);
|
||||
// if slug is an alias, fetch the real slug
|
||||
if (!versions.length) {
|
||||
// unalias device type slug
|
||||
slug = (await sdk.models.deviceType.get(slug, { $select: 'slug' })).slug;
|
||||
// unaliasDeviceType() produces a nice error msg if slug is invalid
|
||||
slug = await unaliasDeviceType(sdk, slug);
|
||||
if (slug !== deviceType) {
|
||||
versions = await sdk.models.os.getAvailableOsVersions(slug);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import { lowercaseIfSlug } from './normalization';
|
||||
|
||||
export const fleetRequired = {
|
||||
name: 'fleet',
|
||||
description: 'fleet name or slug (preferred)',
|
||||
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
|
||||
required: true,
|
||||
parse: lowercaseIfSlug,
|
||||
};
|
||||
|
@ -19,12 +19,13 @@ import { flags } from '@oclif/command';
|
||||
import { stripIndent } from './lazy';
|
||||
import { lowercaseIfSlug } from './normalization';
|
||||
|
||||
import { isV14 } from './version';
|
||||
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
||||
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
|
||||
|
||||
export const fleet = flags.string({
|
||||
char: 'f',
|
||||
description: 'fleet name or slug (preferred)',
|
||||
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
|
||||
parse: lowercaseIfSlug,
|
||||
});
|
||||
|
||||
@ -74,12 +75,6 @@ export const dev: IBooleanFlag<boolean> = flags.boolean({
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const secureBoot: IBooleanFlag<boolean> = flags.boolean({
|
||||
description:
|
||||
'Configure balenaOS installer to opt-in secure boot and disk encryption',
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const drive = flags.string({
|
||||
char: 'd',
|
||||
description: stripIndent`
|
||||
@ -102,6 +97,19 @@ export const deviceType = flags.string({
|
||||
required: true,
|
||||
});
|
||||
|
||||
export const deviceTypeIgnored = {
|
||||
...(isV14()
|
||||
? {}
|
||||
: {
|
||||
type: flags.string({
|
||||
description: 'ignored - no longer required',
|
||||
char: 't',
|
||||
required: false,
|
||||
hidden: true,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export const json: IBooleanFlag<boolean> = flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
|
14
lib/utils/compose-types.d.ts
vendored
14
lib/utils/compose-types.d.ts
vendored
@ -15,11 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ImageModel,
|
||||
ReleaseModel,
|
||||
} from '@balena/compose/dist/release/models';
|
||||
import type { Composition, ImageDescriptor } from '@balena/compose/dist/parse';
|
||||
import type { ImageModel, ReleaseModel } from 'balena-release/build/models';
|
||||
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
|
||||
import type { Pack } from 'tar-stream';
|
||||
|
||||
interface Image {
|
||||
@ -42,7 +39,7 @@ export interface BuiltImage {
|
||||
|
||||
export interface TaggedImage {
|
||||
localImage: import('dockerode').Image;
|
||||
serviceImage: import('@balena/compose/dist/release/models').ImageModel;
|
||||
serviceImage: import('balena-release/build/models').ImageModel;
|
||||
serviceName: string;
|
||||
logs: string;
|
||||
props: BuiltImage.props;
|
||||
@ -64,6 +61,7 @@ export interface ComposeOpts {
|
||||
export interface ComposeCliFlags {
|
||||
emulated: boolean;
|
||||
dockerfile?: string;
|
||||
logs: boolean;
|
||||
nologs: boolean;
|
||||
'multi-dockerignore': boolean;
|
||||
'noparent-check': boolean;
|
||||
@ -80,9 +78,7 @@ export interface ComposeProject {
|
||||
}
|
||||
|
||||
export interface Release {
|
||||
client: ReturnType<
|
||||
typeof import('@balena/compose/dist/release').createClient
|
||||
>;
|
||||
client: ReturnType<typeof import('balena-release').createClient>;
|
||||
release: Pick<
|
||||
ReleaseModel,
|
||||
| 'id'
|
||||
|
@ -19,7 +19,7 @@ import type { Renderer } from './compose_ts';
|
||||
import type * as SDK from 'balena-sdk';
|
||||
import type Dockerode = require('dockerode');
|
||||
import * as path from 'path';
|
||||
import type { Composition, ImageDescriptor } from '@balena/compose/dist/parse';
|
||||
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
|
||||
import type {
|
||||
BuiltImage,
|
||||
ComposeOpts,
|
||||
@ -64,7 +64,7 @@ export function createProject(
|
||||
): ComposeProject {
|
||||
const yml = require('js-yaml') as typeof import('js-yaml');
|
||||
const compose =
|
||||
require('@balena/compose/dist/parse') as typeof import('@balena/compose/dist/parse');
|
||||
require('resin-compose-parse') as typeof import('resin-compose-parse');
|
||||
|
||||
// both methods below may throw.
|
||||
const rawComposition = yml.load(composeStr);
|
||||
@ -107,7 +107,7 @@ export const createRelease = async function (
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
const crypto = require('crypto') as typeof import('crypto');
|
||||
const releaseMod =
|
||||
require('@balena/compose/dist/release') as typeof import('@balena/compose/dist/release');
|
||||
require('balena-release') as typeof import('balena-release');
|
||||
|
||||
const client = releaseMod.createClient({ apiEndpoint, auth });
|
||||
|
||||
@ -214,7 +214,7 @@ export const getPreviousRepos = (
|
||||
image: [SDK.Image];
|
||||
}>;
|
||||
const { getRegistryAndName } =
|
||||
require('@balena/compose/dist/multibuild') as typeof import('@balena/compose/dist/multibuild');
|
||||
require('resin-multibuild') as typeof import('resin-multibuild');
|
||||
return Promise.all(
|
||||
images.map(function (d) {
|
||||
const imageName = d.image[0].is_stored_at__image_location || '';
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import { BalenaSDK } from 'balena-sdk';
|
||||
import type { TransposeOptions } from '@balena/compose/dist/emulate';
|
||||
import type { TransposeOptions } from 'docker-qemu-transpose';
|
||||
import type * as Dockerode from 'dockerode';
|
||||
import { promises as fs } from 'fs';
|
||||
import jsyaml = require('js-yaml');
|
||||
@ -26,9 +26,8 @@ import type {
|
||||
BuildConfig,
|
||||
Composition,
|
||||
ImageDescriptor,
|
||||
} from '@balena/compose/dist/parse';
|
||||
import type * as MultiBuild from '@balena/compose/dist/multibuild';
|
||||
import * as semver from 'semver';
|
||||
} from 'resin-compose-parse';
|
||||
import type * as MultiBuild from 'resin-multibuild';
|
||||
import type { Duplex, Readable } from 'stream';
|
||||
import type { Pack } from 'tar-stream';
|
||||
|
||||
@ -118,7 +117,7 @@ export async function loadProject(
|
||||
image?: string,
|
||||
imageTag?: string,
|
||||
): Promise<ComposeProject> {
|
||||
const compose = await import('@balena/compose/dist/parse');
|
||||
const compose = await import('resin-compose-parse');
|
||||
const { createProject } = await import('./compose');
|
||||
let composeName: string;
|
||||
let composeStr: string;
|
||||
@ -262,7 +261,7 @@ export async function buildProject(
|
||||
opts: BuildProjectOpts,
|
||||
): Promise<BuiltImage[]> {
|
||||
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
|
||||
const compose = await import('@balena/compose/dist/parse');
|
||||
const compose = await import('resin-compose-parse');
|
||||
const imageDescriptors = compose.parse(opts.composition);
|
||||
const renderer = await startRenderer({ imageDescriptors, ...opts });
|
||||
let buildSummaryByService: Dictionary<string> | undefined;
|
||||
@ -333,7 +332,7 @@ async function $buildProject(
|
||||
logger.logDebug('Prepared tasks; building...');
|
||||
|
||||
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
|
||||
const builder = await import('@balena/compose/dist/multibuild');
|
||||
const builder = await import('resin-multibuild');
|
||||
|
||||
const builtImages = await builder.performBuilds(
|
||||
tasks,
|
||||
@ -481,9 +480,8 @@ async function qemuTransposeBuildStream({
|
||||
throw new Error(`No buildStream for task '${task.tag}'`);
|
||||
}
|
||||
|
||||
const transpose = await import('@balena/compose/dist/emulate');
|
||||
const { toPosixPath } = (await import('@balena/compose/dist/multibuild'))
|
||||
.PathUtils;
|
||||
const transpose = await import('docker-qemu-transpose');
|
||||
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
|
||||
|
||||
const transposeOptions: TransposeOptions = {
|
||||
hostQemuPath: toPosixPath(binPath),
|
||||
@ -509,9 +507,9 @@ async function setTaskProgressHooks({
|
||||
inlineLogs?: boolean;
|
||||
renderer: Renderer;
|
||||
task: BuildTaskPlus;
|
||||
transposeOptions?: import('@balena/compose/dist/emulate').TransposeOptions;
|
||||
transposeOptions?: import('docker-qemu-transpose').TransposeOptions;
|
||||
}) {
|
||||
const transpose = await import('@balena/compose/dist/emulate');
|
||||
const transpose = await import('docker-qemu-transpose');
|
||||
// Get the service-specific log stream
|
||||
const logStream = renderer.streams[task.serviceName];
|
||||
task.logBuffer = [];
|
||||
@ -725,16 +723,16 @@ export async function getServiceDirsFromComposition(
|
||||
*
|
||||
* The `image` argument may therefore refer to either a `build` or `image` property
|
||||
* of a service in a docker-compose.yml file, which is a bit confusing but it matches
|
||||
* the `ImageDescriptor.image` property as defined by `@balena/compose/parse`.
|
||||
* the `ImageDescriptor.image` property as defined by `resin-compose-parse`.
|
||||
*
|
||||
* Note that `@balena/compose/parse` "normalizes" the docker-compose.yml file such
|
||||
* Note that `resin-compose-parse` "normalizes" the docker-compose.yml file such
|
||||
* that, if `services.service.build` is a string, it is converted to a BuildConfig
|
||||
* object with the string value assigned to `services.service.build.context`:
|
||||
* https://github.com/balena-io-modules/balena-compose/blob/v0.1.0/lib/parse/compose.ts#L166-L167
|
||||
* https://github.com/balena-io-modules/resin-compose-parse/blob/v2.1.3/src/compose.ts#L166-L167
|
||||
* This is why this implementation works when `services.service.build` is defined
|
||||
* as a string in the docker-compose.yml file.
|
||||
*
|
||||
* @param image The `ImageDescriptor.image` attribute parsed with `@balena/compose/parse`
|
||||
* @param image The `ImageDescriptor.image` attribute parsed with `resin-compose-parse`
|
||||
*/
|
||||
export function isBuildConfig(
|
||||
image: string | BuildConfig,
|
||||
@ -760,8 +758,7 @@ export async function tarDirectory(
|
||||
}: TarDirectoryOptions,
|
||||
): Promise<import('stream').Readable> {
|
||||
const { filterFilesWithDockerignore } = await import('./ignore');
|
||||
const { toPosixPath } = (await import('@balena/compose/dist/multibuild'))
|
||||
.PathUtils;
|
||||
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
|
||||
|
||||
let readFile: (file: string) => Promise<Buffer>;
|
||||
if (process.platform === 'win32') {
|
||||
@ -943,7 +940,7 @@ async function parseRegistrySecrets(
|
||||
throw new ExpectedError('Filename must end with .json, .yml or .yaml');
|
||||
}
|
||||
const raw = (await fs.readFile(secretsFilename)).toString();
|
||||
const multiBuild = await import('@balena/compose/dist/multibuild');
|
||||
const multiBuild = await import('resin-multibuild');
|
||||
const registrySecrets =
|
||||
new multiBuild.RegistrySecretValidator().validateRegistrySecrets(
|
||||
isYaml ? require('js-yaml').load(raw) : JSON.parse(raw),
|
||||
@ -972,7 +969,7 @@ export async function makeBuildTasks(
|
||||
releaseHash: string = 'unavailable',
|
||||
preprocessHook?: (dockerfile: string) => string,
|
||||
): Promise<MultiBuild.BuildTask[]> {
|
||||
const multiBuild = await import('@balena/compose/dist/multibuild');
|
||||
const multiBuild = await import('resin-multibuild');
|
||||
const buildTasks = await multiBuild.splitBuildStream(composition, tarStream);
|
||||
|
||||
logger.logDebug('Found build tasks:');
|
||||
@ -1018,7 +1015,7 @@ async function performResolution(
|
||||
releaseHash: string,
|
||||
preprocessHook?: (dockerfile: string) => string,
|
||||
): Promise<MultiBuild.BuildTask[]> {
|
||||
const multiBuild = await import('@balena/compose/dist/multibuild');
|
||||
const multiBuild = await import('resin-multibuild');
|
||||
const resolveListeners: MultiBuild.ResolveListeners = {};
|
||||
const resolvePromise = new Promise<never>((_resolve, reject) => {
|
||||
resolveListeners.error = [reject];
|
||||
@ -1083,7 +1080,7 @@ async function validateSpecifiedDockerfile(
|
||||
dockerfilePath: string,
|
||||
): Promise<string> {
|
||||
const { contains, toNativePath, toPosixPath } = (
|
||||
await import('@balena/compose/dist/multibuild')
|
||||
await import('resin-multibuild')
|
||||
).PathUtils;
|
||||
|
||||
const nativeProjectPath = path.normalize(projectPath);
|
||||
@ -1243,7 +1240,7 @@ async function pushAndUpdateServiceImages(
|
||||
token: string,
|
||||
images: TaggedImage[],
|
||||
afterEach: (
|
||||
serviceImage: import('@balena/compose/dist/release/models').ImageModel,
|
||||
serviceImage: import('balena-release/build/models').ImageModel,
|
||||
props: object,
|
||||
) => void,
|
||||
) {
|
||||
@ -1328,14 +1325,12 @@ async function pushAndUpdateServiceImages(
|
||||
async function pushServiceImages(
|
||||
docker: Dockerode,
|
||||
logger: Logger,
|
||||
pineClient: ReturnType<
|
||||
typeof import('@balena/compose/dist/release').createClient
|
||||
>,
|
||||
pineClient: ReturnType<typeof import('balena-release').createClient>,
|
||||
taggedImages: TaggedImage[],
|
||||
token: string,
|
||||
skipLogUpload: boolean,
|
||||
): Promise<void> {
|
||||
const releaseMod = await import('@balena/compose/dist/release');
|
||||
const releaseMod = await import('balena-release');
|
||||
logger.logInfo('Pushing images to registry...');
|
||||
await pushAndUpdateServiceImages(
|
||||
docker,
|
||||
@ -1353,6 +1348,9 @@ async function pushServiceImages(
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: This should be shared between the CLI & the Builder
|
||||
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
|
||||
|
||||
export async function deployProject(
|
||||
docker: Dockerode,
|
||||
logger: Logger,
|
||||
@ -1365,8 +1363,8 @@ export async function deployProject(
|
||||
skipLogUpload: boolean,
|
||||
projectPath: string,
|
||||
isDraft: boolean,
|
||||
): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
|
||||
const releaseMod = await import('@balena/compose/dist/release');
|
||||
): Promise<import('balena-release/build/models').ReleaseModel> {
|
||||
const releaseMod = await import('balena-release');
|
||||
const { createRelease, tagServiceImages } = await import('./compose');
|
||||
const tty = (await import('./tty'))(process.stdout);
|
||||
|
||||
@ -1375,10 +1373,10 @@ export async function deployProject(
|
||||
|
||||
const contractPath = path.join(projectPath, 'balena.yml');
|
||||
const contract = await getContractContent(contractPath);
|
||||
if (contract?.version && !semver.valid(contract.version)) {
|
||||
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
|
||||
throw new ExpectedError(stripIndent`\
|
||||
Error: the version field in "${contractPath}"
|
||||
is not a valid semver`);
|
||||
Error: expected the version field in "${contractPath}"
|
||||
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
|
||||
}
|
||||
|
||||
const $release = await runSpinner(
|
||||
@ -1653,6 +1651,10 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
|
||||
description:
|
||||
'Alternative Dockerfile name/path, relative to the source folder',
|
||||
}),
|
||||
logs: flags.boolean({
|
||||
description:
|
||||
'No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.',
|
||||
}),
|
||||
nologs: flags.boolean({
|
||||
description:
|
||||
'Hide the image build log output (produce less verbose output)',
|
||||
|
@ -52,10 +52,6 @@ export interface ImgConfig {
|
||||
os?: {
|
||||
sshKeys?: string[];
|
||||
};
|
||||
|
||||
installer?: {
|
||||
secureboot?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateApplicationConfig(
|
||||
@ -67,7 +63,6 @@ export async function generateApplicationConfig(
|
||||
os?: {
|
||||
sshKeys?: string[];
|
||||
};
|
||||
secureBoot?: boolean;
|
||||
},
|
||||
): Promise<ImgConfig> {
|
||||
options = {
|
||||
@ -89,12 +84,6 @@ export async function generateApplicationConfig(
|
||||
: options.os.sshKeys;
|
||||
}
|
||||
|
||||
// configure installer secure boot opt-in if specified
|
||||
if (options.secureBoot) {
|
||||
config.installer ??= {};
|
||||
config.installer.secureboot = options.secureBoot;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@ -176,62 +165,3 @@ export async function validateDevOptionAndWarn(
|
||||
and exposes network ports such as 2375 that allows unencrypted access to balenaEngine.
|
||||
Therefore, development mode should only be used in private, trusted local networks.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chech whether the `--secureBoot` option of commands related to OS configuration
|
||||
* such as `os configure` and `config generate` is compatible with a given
|
||||
* OS release, and print a warning regarding the consequences of using that
|
||||
* option.
|
||||
*/
|
||||
export async function validateSecureBootOptionAndWarn(
|
||||
secureBoot?: boolean,
|
||||
slug?: string,
|
||||
version?: string,
|
||||
logger?: import('./logger'),
|
||||
) {
|
||||
if (!secureBoot) {
|
||||
return;
|
||||
}
|
||||
const { ExpectedError } = await import('../errors');
|
||||
if (!version) {
|
||||
throw new ExpectedError(`Error: No version provided`);
|
||||
}
|
||||
if (!slug) {
|
||||
throw new ExpectedError(`Error: No device type provided`);
|
||||
}
|
||||
const sdk = getBalenaSdk();
|
||||
const releasePineOpts = {
|
||||
$select: 'contract',
|
||||
$filter: { raw_version: `${version.replace(/^v/, '')}` },
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.Release>;
|
||||
// TODO: Remove the added type casting when the SDK returns the fully typed result
|
||||
const [osRelease] = (await sdk.models.os.getAllOsVersions(
|
||||
slug,
|
||||
releasePineOpts,
|
||||
)) as Array<
|
||||
BalenaSdk.OsVersion &
|
||||
BalenaSdk.PineTypedResult<BalenaSdk.Release, typeof releasePineOpts>
|
||||
>;
|
||||
if (!osRelease) {
|
||||
throw new ExpectedError(`Error: No ${version} release for ${slug}`);
|
||||
}
|
||||
|
||||
const contract = osRelease.contract ? JSON.parse(osRelease.contract) : null;
|
||||
if (
|
||||
contract?.provides.some((entry: Dictionary<string>) => {
|
||||
return entry.type === 'sw.feature' && entry.slug === 'secureboot';
|
||||
})
|
||||
) {
|
||||
if (!logger) {
|
||||
const Logger = await import('./logger');
|
||||
logger = Logger.getLogger();
|
||||
}
|
||||
logger.logInfo(stripIndent`
|
||||
The '--secureBoot' option is being used to configure a balenaOS installer image
|
||||
into secure boot and full disk encryption.`);
|
||||
} else {
|
||||
throw new ExpectedError(
|
||||
`Error: The '--secureBoot' option is not supported for ${slug} in ${version}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,13 @@
|
||||
import * as semver from 'balena-semver';
|
||||
import * as Docker from 'dockerode';
|
||||
import * as _ from 'lodash';
|
||||
import { Composition } from '@balena/compose/dist/parse';
|
||||
import { Composition } from 'resin-compose-parse';
|
||||
import {
|
||||
BuildTask,
|
||||
getAuthConfigObj,
|
||||
LocalImage,
|
||||
RegistrySecrets,
|
||||
} from '@balena/compose/dist/multibuild';
|
||||
} from 'resin-multibuild';
|
||||
import type { Readable } from 'stream';
|
||||
|
||||
import { BALENA_ENGINE_TMP_PATH } from '../../config';
|
||||
@ -44,7 +44,7 @@ import { displayBuildLog } from './logs';
|
||||
import { stripIndent } from '../lazy';
|
||||
|
||||
const LOCAL_APPNAME = 'localapp';
|
||||
const LOCAL_RELEASEHASH = '10ca12e1ea5e';
|
||||
const LOCAL_RELEASEHASH = 'localrelease';
|
||||
const LOCAL_PROJECT_NAME = 'local_image';
|
||||
|
||||
// Define the logger here so the debug output
|
||||
@ -209,9 +209,9 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
globalLogger.logDebug('Fetching device information...');
|
||||
const deviceInfo = await api.getDeviceInformation();
|
||||
|
||||
let imageIds: Dictionary<string[]> | undefined;
|
||||
let buildLogs: Dictionary<string> | undefined;
|
||||
if (!opts.nolive) {
|
||||
imageIds = {};
|
||||
buildLogs = {};
|
||||
}
|
||||
|
||||
const { awaitInterruptibleTask } = await import('../helpers');
|
||||
@ -223,7 +223,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
deviceInfo,
|
||||
globalLogger,
|
||||
opts,
|
||||
imageIds,
|
||||
buildLogs,
|
||||
);
|
||||
|
||||
globalLogger.outputDeferredMessages();
|
||||
@ -265,7 +265,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
docker,
|
||||
logger: globalLogger,
|
||||
composition: project.composition,
|
||||
imageIds: imageIds!,
|
||||
buildLogs: buildLogs!,
|
||||
deployOpts: opts,
|
||||
});
|
||||
promises.push(livepush.init());
|
||||
@ -312,14 +312,6 @@ function connectToDocker(host: string, port: number): Docker {
|
||||
});
|
||||
}
|
||||
|
||||
function extractDockerArrowMessage(outputLine: string): string | undefined {
|
||||
const arrowTest = /^.*\s*-+>\s*(.+)/i;
|
||||
const match = arrowTest.exec(outputLine);
|
||||
if (match != null) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
async function performBuilds(
|
||||
composition: Composition,
|
||||
tarStream: Readable,
|
||||
@ -327,9 +319,9 @@ async function performBuilds(
|
||||
deviceInfo: DeviceInfo,
|
||||
logger: Logger,
|
||||
opts: DeviceDeployOptions,
|
||||
imageIds?: Dictionary<string[]>,
|
||||
buildLogs?: Dictionary<string>,
|
||||
): Promise<BuildTask[]> {
|
||||
const multibuild = await import('@balena/compose/dist/multibuild');
|
||||
const multibuild = await import('resin-multibuild');
|
||||
|
||||
const buildTasks = await makeBuildTasks(
|
||||
composition,
|
||||
@ -353,29 +345,14 @@ async function performBuilds(
|
||||
// If we're passed a build logs object make sure to set it
|
||||
// up properly
|
||||
let logHandlers: ((serviceName: string, line: string) => void) | undefined;
|
||||
|
||||
const lastArrowMessage: Dictionary<string> = {};
|
||||
|
||||
if (imageIds != null) {
|
||||
if (buildLogs != null) {
|
||||
for (const task of buildTasks) {
|
||||
if (!task.external) {
|
||||
imageIds[task.serviceName] = [];
|
||||
buildLogs[task.serviceName] = '';
|
||||
}
|
||||
}
|
||||
logHandlers = (serviceName: string, line: string) => {
|
||||
// If this was a from line, take the last found
|
||||
// image id and save it
|
||||
if (
|
||||
/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
|
||||
lastArrowMessage[serviceName] != null
|
||||
) {
|
||||
imageIds[serviceName].push(lastArrowMessage[serviceName]);
|
||||
} else {
|
||||
const msg = extractDockerArrowMessage(line);
|
||||
if (msg != null) {
|
||||
lastArrowMessage[serviceName] = msg;
|
||||
}
|
||||
}
|
||||
buildLogs[serviceName] += `${line}\n`;
|
||||
};
|
||||
}
|
||||
|
||||
@ -393,7 +370,7 @@ async function performBuilds(
|
||||
const imagesToRemove: string[] = [];
|
||||
|
||||
// Now tag any external images with the correct name that they should be,
|
||||
// as this won't be done by @balena/compose/multibuild
|
||||
// as this won't be done by resin-multibuild
|
||||
await Promise.all(
|
||||
localImages.map(async (localImage) => {
|
||||
if (localImage.external) {
|
||||
@ -436,26 +413,12 @@ export async function rebuildSingleTask(
|
||||
// the logs, so any calller who wants to keep track of
|
||||
// this should provide the following callback
|
||||
containerIdCb?: (id: string) => void,
|
||||
): Promise<string[]> {
|
||||
const multibuild = await import('@balena/compose/dist/multibuild');
|
||||
): Promise<string> {
|
||||
const multibuild = await import('resin-multibuild');
|
||||
// First we run the build task, to get the new image id
|
||||
const stageIds = [] as string[];
|
||||
let lastArrowMessage: string | undefined;
|
||||
|
||||
let buildLogs = '';
|
||||
const logHandler = (_s: string, line: string) => {
|
||||
// If this was a FROM line, take the last found
|
||||
// image id and save it as a stage id
|
||||
if (
|
||||
/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
|
||||
lastArrowMessage != null
|
||||
) {
|
||||
stageIds.push(lastArrowMessage);
|
||||
} else {
|
||||
const msg = extractDockerArrowMessage(line);
|
||||
if (msg != null) {
|
||||
lastArrowMessage = msg;
|
||||
}
|
||||
}
|
||||
buildLogs += `${line}\n`;
|
||||
|
||||
if (containerIdCb != null) {
|
||||
const match = line.match(/^\s*--->\s*Running\s*in\s*([a-f0-9]*)\s*$/i);
|
||||
@ -514,7 +477,7 @@ export async function rebuildSingleTask(
|
||||
]);
|
||||
}
|
||||
|
||||
return stageIds;
|
||||
return buildLogs;
|
||||
}
|
||||
|
||||
function assignOutputHandlers(
|
||||
@ -570,17 +533,10 @@ async function assignDockerBuildOpts(
|
||||
await Promise.all(
|
||||
buildTasks.map(async (task: BuildTask) => {
|
||||
task.dockerOpts = {
|
||||
...(task.dockerOpts || {}),
|
||||
...{
|
||||
cachefrom: images,
|
||||
labels: {
|
||||
'io.resin.local.image': '1',
|
||||
'io.resin.local.service': task.serviceName,
|
||||
},
|
||||
t: getImageNameFromTask(task),
|
||||
nocache: opts.nocache,
|
||||
forcerm: true,
|
||||
pull: opts.pull,
|
||||
cachefrom: images,
|
||||
labels: {
|
||||
'io.resin.local.image': '1',
|
||||
'io.resin.local.service': task.serviceName,
|
||||
},
|
||||
t: getImageNameFromTask(task),
|
||||
nocache: opts.nocache,
|
||||
|
@ -21,8 +21,8 @@ import * as fs from 'fs';
|
||||
import Livepush, { ContainerNotRunningError } from 'livepush';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import type { Composition } from '@balena/compose/dist/parse';
|
||||
import type { BuildTask } from '@balena/compose/dist/multibuild';
|
||||
import type { Composition } from 'resin-compose-parse';
|
||||
import type { BuildTask } from 'resin-multibuild';
|
||||
|
||||
import { instanceOf } from '../../errors';
|
||||
import Logger = require('../logger');
|
||||
@ -52,6 +52,7 @@ interface MonitoredContainer {
|
||||
containerId: string;
|
||||
}
|
||||
|
||||
type BuildLogs = Dictionary<string>;
|
||||
type StageImageIDs = Dictionary<string[]>;
|
||||
|
||||
export interface LivepushOpts {
|
||||
@ -61,7 +62,7 @@ export interface LivepushOpts {
|
||||
docker: Dockerode;
|
||||
api: DeviceAPI;
|
||||
logger: Logger;
|
||||
imageIds: StageImageIDs;
|
||||
buildLogs: BuildLogs;
|
||||
deployOpts: DeviceDeployOptions;
|
||||
}
|
||||
|
||||
@ -96,7 +97,7 @@ export class LivepushManager {
|
||||
this.api = opts.api;
|
||||
this.logger = opts.logger;
|
||||
this.deployOpts = opts.deployOpts;
|
||||
this.imageIds = opts.imageIds;
|
||||
this.imageIds = LivepushManager.getMultistageImageIDs(opts.buildLogs);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
@ -249,7 +250,7 @@ export class LivepushManager {
|
||||
cwd: serviceContext,
|
||||
followSymlinks: true,
|
||||
ignoreInitial: true,
|
||||
ignored: (filePath: string, stats?: fs.Stats) => {
|
||||
ignored: (filePath: string, stats: fs.Stats | undefined) => {
|
||||
if (!stats) {
|
||||
try {
|
||||
// sync because chokidar defines a sync interface
|
||||
@ -296,6 +297,33 @@ export class LivepushManager {
|
||||
return new Dockerfile(content).generateLiveDockerfile();
|
||||
}
|
||||
|
||||
private static getMultistageImageIDs(buildLogs: BuildLogs): StageImageIDs {
|
||||
const stageIds: StageImageIDs = {};
|
||||
_.each(buildLogs, (log, serviceName) => {
|
||||
stageIds[serviceName] = [];
|
||||
|
||||
const lines = log.split(/\r?\n/);
|
||||
let lastArrowMessage: string | undefined;
|
||||
for (const line of lines) {
|
||||
// If this was a from line, take the last found
|
||||
// image id and save it
|
||||
if (
|
||||
/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
|
||||
lastArrowMessage != null
|
||||
) {
|
||||
stageIds[serviceName].push(lastArrowMessage);
|
||||
} else {
|
||||
const msg = LivepushManager.extractDockerArrowMessage(line);
|
||||
if (msg != null) {
|
||||
lastArrowMessage = msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return stageIds;
|
||||
}
|
||||
|
||||
private async awaitDeviceStateSettle(): Promise<void> {
|
||||
// Cache the state to avoid unnecessary calls
|
||||
this.lastDeviceStatus = await this.api.getStatus();
|
||||
@ -377,9 +405,9 @@ export class LivepushManager {
|
||||
);
|
||||
}
|
||||
|
||||
let stageImages: string[];
|
||||
let buildLog: string;
|
||||
try {
|
||||
stageImages = await rebuildSingleTask(
|
||||
buildLog = await rebuildSingleTask(
|
||||
serviceName,
|
||||
this.docker,
|
||||
this.logger,
|
||||
@ -438,13 +466,17 @@ export class LivepushManager {
|
||||
);
|
||||
}
|
||||
|
||||
const buildLogs: Dictionary<string> = {};
|
||||
buildLogs[serviceName] = buildLog;
|
||||
const stageImages = LivepushManager.getMultistageImageIDs(buildLogs);
|
||||
|
||||
const dockerfile = new Dockerfile(buildTask.dockerfile!);
|
||||
|
||||
instance.livepush = await Livepush.init({
|
||||
dockerfile,
|
||||
context: buildTask.context!,
|
||||
containerId: container.containerId,
|
||||
stageImages,
|
||||
stageImages: stageImages[serviceName],
|
||||
docker: this.docker,
|
||||
});
|
||||
this.assignLivepushOutputHandlers(serviceName, instance.livepush);
|
||||
@ -504,6 +536,16 @@ export class LivepushManager {
|
||||
});
|
||||
}
|
||||
|
||||
private static extractDockerArrowMessage(
|
||||
outputLine: string,
|
||||
): string | undefined {
|
||||
const arrowTest = /^.*\s*-+>\s*(.+)/i;
|
||||
const match = arrowTest.exec(outputLine);
|
||||
if (match != null) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
private getDockerfilePathFromTask(task: BuildTask): string[] {
|
||||
switch (task.projectType) {
|
||||
case 'Standard Dockerfile':
|
||||
|
@ -155,8 +155,12 @@ export function displayLogObject<T extends Log>(
|
||||
system: boolean,
|
||||
filterServices?: string[],
|
||||
): void {
|
||||
const d = obj.timestamp != null ? new Date(obj.timestamp) : new Date();
|
||||
let toPrint = `[${d.toISOString()}]`;
|
||||
let toPrint: string;
|
||||
if (obj.timestamp != null) {
|
||||
toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`;
|
||||
} else {
|
||||
toPrint = `[${new Date().toLocaleString()}]`;
|
||||
}
|
||||
|
||||
if (obj.serviceName != null) {
|
||||
if (filterServices) {
|
||||
|
@ -13,140 +13,89 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExpectedError } from '../../errors';
|
||||
import type { ContainerInfo } from 'dockerode';
|
||||
import { stripIndent } from '../lazy';
|
||||
|
||||
import {
|
||||
findBestUsernameForDevice,
|
||||
getRemoteCommandOutput,
|
||||
runRemoteCommand,
|
||||
SshRemoteCommandOpts,
|
||||
} from '../ssh';
|
||||
|
||||
export interface DeviceSSHOpts extends SshRemoteCommandOpts {
|
||||
export interface DeviceSSHOpts {
|
||||
address: string;
|
||||
port?: number;
|
||||
forceTTY?: boolean;
|
||||
verbose: boolean;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
|
||||
|
||||
/**
|
||||
* List the running containers on the device over ssh, and return the full
|
||||
* container name that matches the given service name.
|
||||
*
|
||||
* Note: In the past, two other approaches were implemented for this function:
|
||||
*
|
||||
* - Obtaining container IDs through a supervisor API call:
|
||||
* '/supervisor/v2/containerId' endpoint, via cloud.
|
||||
* - Obtaining container IDs using 'dockerode' connected directly to
|
||||
* balenaEngine on a device, TCP port 2375.
|
||||
*
|
||||
* The problem with using the supervisor API is that it means that 'balena ssh'
|
||||
* becomes dependent on the supervisor being up an running, but sometimes ssh
|
||||
* is needed to investigate devices issues where the supervisor has got into
|
||||
* trouble (e.g. supervisor in restart loop). This is the subject of CLI issue
|
||||
* https://github.com/balena-io/balena-cli/issues/1560 .
|
||||
*
|
||||
* The problem with using dockerode to connect directly to port 2375 (balenaEngine)
|
||||
* is that it only works with development variants of balenaOS. Production variants
|
||||
* block access to port 2375 for security reasons. 'balena ssh' should support
|
||||
* production variants as well, especially after balenaOS v2.44.0 that introduced
|
||||
* support for using the cloud account username for ssh authentication.
|
||||
*
|
||||
* Overall, the most reliable approach is to run 'balena-engine ps' over ssh.
|
||||
* It is OK to depend on balenaEngine because ssh to a container is implemented
|
||||
* through 'balena-engine exec' anyway, and of course it is OK to depend on ssh
|
||||
* itself.
|
||||
*/
|
||||
export async function getContainerIdForService(
|
||||
opts: SshRemoteCommandOpts & { service: string; deviceUuid?: string },
|
||||
): Promise<string> {
|
||||
opts.cmd = `"${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`;
|
||||
if (opts.deviceUuid) {
|
||||
// If a device UUID is given, perform ssh via cloud proxy 'host' command
|
||||
opts.cmd = `host ${opts.deviceUuid} ${opts.cmd}`;
|
||||
}
|
||||
|
||||
const psLines: string[] = (
|
||||
await getRemoteCommandOutput({ ...opts, stderr: 'inherit' })
|
||||
).stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((l) => l);
|
||||
|
||||
const { escapeRegExp } = await import('lodash');
|
||||
const regex = new RegExp(`(?:^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
|
||||
// Old balenaOS container name pattern:
|
||||
// main_1234567_2345678
|
||||
// New balenaOS container name patterns:
|
||||
// main_1234567_2345678_a000b111c222d333e444f555a666b777
|
||||
// main_1_1_localrelease
|
||||
const nameRegex = /(?:^|\/)([a-zA-Z0-9_-]+)_\d+_\d+(?:_.+)?$/;
|
||||
|
||||
const serviceNames: string[] = [];
|
||||
const containerNames: string[] = [];
|
||||
let containerId: string | undefined;
|
||||
|
||||
// sample psLine: 'b603c74e951e bar_4587562_2078151_3261c9d4c22f2c53a5267be459c89990'
|
||||
for (const psLine of psLines) {
|
||||
const [cId, name] = psLine.split(' ');
|
||||
if (cId && name) {
|
||||
if (regex.test(name)) {
|
||||
containerNames.push(name);
|
||||
containerId = cId;
|
||||
}
|
||||
const match = name.match(nameRegex);
|
||||
if (match) {
|
||||
serviceNames.push(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (containerNames.length > 1) {
|
||||
const [s, d] = [opts.service, opts.deviceUuid || opts.hostname];
|
||||
throw new ExpectedError(stripIndent`
|
||||
Found more than one container matching service name "${s}" on device "${d}":
|
||||
${containerNames.join(', ')}
|
||||
Use different service names to avoid ambiguity.
|
||||
`);
|
||||
}
|
||||
if (!containerId) {
|
||||
const [s, d] = [opts.service, opts.deviceUuid || opts.hostname];
|
||||
throw new ExpectedError(
|
||||
`Could not find a container matching service name "${s}" on device "${d}".${
|
||||
serviceNames.length > 0
|
||||
? `\nAvailable services:\n\t${serviceNames.join('\n\t')}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
return containerId;
|
||||
}
|
||||
export const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
|
||||
|
||||
export async function performLocalDeviceSSH(
|
||||
opts: DeviceSSHOpts,
|
||||
): Promise<void> {
|
||||
// Before we started using `findBestUsernameForDevice`, we tried the approach
|
||||
// of attempting ssh with the 'root' username first and, if that failed, then
|
||||
// attempting ssh with a regular user (balenaCloud username). The problem with
|
||||
// that approach was that it would print the following message to the console:
|
||||
// "root@192.168.1.36: Permission denied (publickey)"
|
||||
// ... right before having success as a regular user, which looked broken or
|
||||
// confusing from users' point of view. Capturing stderr to prevent that
|
||||
// message from being printed is tricky because the messages printed to stderr
|
||||
// may include the stderr output of remote commands that are of interest to
|
||||
// the user.
|
||||
const username = await findBestUsernameForDevice(opts.hostname, opts.port);
|
||||
let cmd = '';
|
||||
const { escapeRegExp, reduce } = await import('lodash');
|
||||
const { spawnSshAndThrowOnError } = await import('../ssh');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
if (opts.service) {
|
||||
const containerId = await getContainerIdForService({
|
||||
...opts,
|
||||
service: opts.service,
|
||||
username,
|
||||
let command = '';
|
||||
|
||||
if (opts.service != null) {
|
||||
// Get the containers which are on-device. Currently we
|
||||
// are single application, which means we can assume any
|
||||
// container which fulfills the form of
|
||||
// $serviceName_$appId_$releaseId is what we want. Once
|
||||
// we have multi-app, we should show a dialog which
|
||||
// allows the user to choose the correct container
|
||||
|
||||
const Docker = await import('dockerode');
|
||||
const docker = new Docker({
|
||||
host: opts.address,
|
||||
port: 2375,
|
||||
});
|
||||
|
||||
const regex = new RegExp(`(^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
|
||||
const nameRegex = /\/?([a-zA-Z0-9_-]+)_\d+_\d+/;
|
||||
let allContainers: ContainerInfo[];
|
||||
try {
|
||||
allContainers = await docker.listContainers();
|
||||
} catch (_e) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Could not access docker daemon on device ${opts.address}.
|
||||
Please ensure the device is in local mode.`);
|
||||
}
|
||||
|
||||
const serviceNames: string[] = [];
|
||||
const containers: Array<{ id: string; name: string }> = [];
|
||||
for (const container of allContainers) {
|
||||
for (const name of container.Names) {
|
||||
if (regex.test(name)) {
|
||||
containers.push({ id: container.Id, name });
|
||||
break;
|
||||
}
|
||||
const match = name.match(nameRegex);
|
||||
if (match) {
|
||||
serviceNames.push(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containers.length === 0) {
|
||||
throw new ExpectedError(
|
||||
`Could not find a service on device with name ${opts.service}. ${
|
||||
serviceNames.length > 0
|
||||
? `Available services:\n${reduce(
|
||||
serviceNames,
|
||||
(str, name) => `${str}\t${name}\n`,
|
||||
'',
|
||||
)}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
if (containers.length > 1) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Found more than one container matching service name "${opts.service}":
|
||||
${containers.map((container) => container.name).join(', ')}
|
||||
Use different service names to avoid ambiguity.
|
||||
`);
|
||||
}
|
||||
|
||||
const containerId = containers[0].id;
|
||||
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
|
||||
// stdin (fd=0) is not a tty when data is piped in, for example
|
||||
// echo 'ls -la; exit;' | balena ssh 192.168.0.20 service1
|
||||
@ -154,8 +103,17 @@ export async function performLocalDeviceSSH(
|
||||
// https://assets.balena.io/newsletter/2020-01/pipe.png
|
||||
const isTTY = !!opts.forceTTY || (await import('tty')).isatty(0);
|
||||
const ttyFlag = isTTY ? '-t' : '';
|
||||
cmd = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
|
||||
command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
|
||||
}
|
||||
|
||||
await runRemoteCommand({ ...opts, cmd, username });
|
||||
return spawnSshAndThrowOnError([
|
||||
...(opts.verbose ? ['-vvv'] : []),
|
||||
'-t',
|
||||
...['-p', opts.port ? opts.port.toString() : '22222'],
|
||||
...['-o', 'LogLevel=ERROR'],
|
||||
...['-o', 'StrictHostKeyChecking=no'],
|
||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||
`root@${opts.address}`,
|
||||
...(command ? [command] : []),
|
||||
]);
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
import { enumerateServices, findServices } from 'resin-discoverable-services';
|
||||
|
||||
interface LocalBalenaOsDevice {
|
||||
address: string;
|
||||
host: string;
|
||||
osVariant?: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
// Although we only check for 'balena-ssh', we know, implicitly, that balenaOS
|
||||
// devices come with 'rsync' installed that can be used over SSH.
|
||||
const avahiBalenaSshTag = 'resin-ssh';
|
||||
|
||||
export async function discoverLocalBalenaOsDevices(
|
||||
timeout = 4000,
|
||||
): Promise<LocalBalenaOsDevice[]> {
|
||||
const availableServices = await enumerateServices();
|
||||
const serviceDefinitions = Array.from(availableServices)
|
||||
.filter((s) => Array.from(s.tags).includes(avahiBalenaSshTag))
|
||||
.map((s) => s.service);
|
||||
|
||||
if (serviceDefinitions.length === 0) {
|
||||
throw new Error(
|
||||
`Could not find any available '${avahiBalenaSshTag}' services`,
|
||||
);
|
||||
}
|
||||
|
||||
const services = await findServices(serviceDefinitions, timeout);
|
||||
return services.map(function (service) {
|
||||
// User referer address to get device IP. This will work fine assuming that
|
||||
// a device only advertises own services.
|
||||
const {
|
||||
referer: { address },
|
||||
host,
|
||||
port,
|
||||
} = service;
|
||||
|
||||
return { address, host, port };
|
||||
});
|
||||
}
|
@ -105,7 +105,7 @@ export interface BuildOpts {
|
||||
cachefrom?: string[];
|
||||
nocache?: boolean;
|
||||
pull?: boolean;
|
||||
registryconfig?: import('@balena/compose/dist/multibuild').RegistrySecrets;
|
||||
registryconfig?: import('resin-multibuild').RegistrySecrets;
|
||||
squash?: boolean;
|
||||
t?: string; // only the tag portion of the image name, e.g. 'abc' in 'myimg:abc'
|
||||
}
|
||||
@ -132,7 +132,7 @@ export function generateBuildOpts(options: {
|
||||
'cache-from'?: string;
|
||||
nocache: boolean;
|
||||
pull?: boolean;
|
||||
'registry-secrets'?: import('@balena/compose/dist/multibuild').RegistrySecrets;
|
||||
'registry-secrets'?: import('resin-multibuild').RegistrySecrets;
|
||||
squash: boolean;
|
||||
tag?: string;
|
||||
}): BuildOpts {
|
||||
@ -174,8 +174,14 @@ export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
|
||||
);
|
||||
}
|
||||
|
||||
export interface ExtendedDockerOptions extends dockerode.DockerOptions {
|
||||
docker?: string; // socket path, e.g. /var/run/docker.sock
|
||||
dockerHost?: string; // host name or IP address
|
||||
dockerPort?: number; // TCP port number, e.g. 2375
|
||||
}
|
||||
|
||||
export async function getDocker(
|
||||
options: DockerConnectionCliFlags,
|
||||
options: ExtendedDockerOptions,
|
||||
): Promise<dockerode> {
|
||||
const connectOpts = await generateConnectOpts(options);
|
||||
const client = await createClient(connectOpts);
|
||||
@ -190,18 +196,14 @@ export async function createClient(
|
||||
return new Docker(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Docker connection options with the default values from the
|
||||
* 'docker-modem' package, which takes several env vars into account,
|
||||
* including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
|
||||
* https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
|
||||
*
|
||||
* @param opts Command line options like --dockerHost and --dockerPort
|
||||
*/
|
||||
export function getDefaultDockerModemOpts(
|
||||
opts: DockerConnectionCliFlags,
|
||||
): dockerode.DockerOptions {
|
||||
const connectOpts: dockerode.DockerOptions = {};
|
||||
async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
||||
let connectOpts: dockerode.DockerOptions = {};
|
||||
|
||||
// Start with docker-modem defaults which take several env vars into account,
|
||||
// including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
|
||||
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
|
||||
const Modem = require('docker-modem');
|
||||
const defaultOpts = new Modem();
|
||||
const optsOfInterest: Array<keyof dockerode.DockerOptions> = [
|
||||
'ca',
|
||||
'cert',
|
||||
@ -213,33 +215,9 @@ export function getDefaultDockerModemOpts(
|
||||
'username',
|
||||
'timeout',
|
||||
];
|
||||
const Modem = require('docker-modem');
|
||||
const originalDockerHost = process.env.DOCKER_HOST;
|
||||
try {
|
||||
if (opts.dockerHost) {
|
||||
process.env.DOCKER_HOST ||= opts.dockerPort
|
||||
? `${opts.dockerHost}:${opts.dockerPort}`
|
||||
: opts.dockerHost;
|
||||
}
|
||||
const defaultOpts = new Modem();
|
||||
for (const opt of optsOfInterest) {
|
||||
connectOpts[opt] = defaultOpts[opt];
|
||||
}
|
||||
} finally {
|
||||
// Did you know? Any value assigned to `process.env.XXX` becomes a string.
|
||||
// For example, `process.env.DOCKER_HOST = undefined` results in
|
||||
// value 'undefined' (a 9-character string) being assigned.
|
||||
if (originalDockerHost) {
|
||||
process.env.DOCKER_HOST = originalDockerHost;
|
||||
} else {
|
||||
delete process.env.DOCKER_HOST;
|
||||
}
|
||||
for (const opt of optsOfInterest) {
|
||||
connectOpts[opt] = defaultOpts[opt];
|
||||
}
|
||||
return connectOpts;
|
||||
}
|
||||
|
||||
export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
|
||||
let connectOpts = getDefaultDockerModemOpts(opts);
|
||||
|
||||
// Now override the default options with any explicit command line options
|
||||
if (opts.docker != null && opts.dockerHost == null) {
|
||||
@ -263,9 +241,9 @@ export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
|
||||
// These should be file paths (strings)
|
||||
const tlsOpts = [opts.ca, opts.cert, opts.key];
|
||||
|
||||
// If any tlsOpts are set...
|
||||
// If any are set...
|
||||
if (tlsOpts.some((opt) => opt)) {
|
||||
// but not all
|
||||
// but not all ()
|
||||
if (!tlsOpts.every((opt) => opt)) {
|
||||
throw new ExpectedError(
|
||||
'You must provide a CA, certificate and key in order to use TLS',
|
||||
@ -280,11 +258,7 @@ export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
|
||||
const [ca, cert, key] = await Promise.all(
|
||||
tlsOpts.map((opt: string) => fs.readFile(opt, 'utf8')),
|
||||
);
|
||||
// Also ensure that the protocol is 'https' like 'docker-modem' does:
|
||||
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L101-L103
|
||||
// TODO: delete redundant logic from this function now that similar logic
|
||||
// exists in the 'docker-modem' package.
|
||||
connectOpts = { ...connectOpts, ca, cert, key, protocol: 'https' };
|
||||
connectOpts = { ...connectOpts, ca, cert, key };
|
||||
}
|
||||
|
||||
return connectOpts;
|
||||
|
@ -107,50 +107,21 @@ export async function getManifest(
|
||||
deviceType: string,
|
||||
): Promise<BalenaSdk.DeviceTypeJson.DeviceType> {
|
||||
const init = await import('balena-device-init');
|
||||
const sdk = getBalenaSdk();
|
||||
const manifest = await init.getImageManifest(image);
|
||||
if (
|
||||
manifest != null &&
|
||||
manifest.slug !== deviceType &&
|
||||
manifest.slug !== (await sdk.models.deviceType.get(deviceType)).slug
|
||||
) {
|
||||
const { ExpectedError } = await import('../errors');
|
||||
throw new ExpectedError(
|
||||
`The device type of the provided OS image ${manifest.slug}, does not match the expected device type ${deviceType}`,
|
||||
);
|
||||
if (manifest != null) {
|
||||
return manifest;
|
||||
}
|
||||
return manifest ?? (await sdk.models.device.getManifestBySlug(deviceType));
|
||||
return getBalenaSdk().models.device.getManifestBySlug(deviceType);
|
||||
}
|
||||
|
||||
export const areDeviceTypesCompatible = async (
|
||||
appDeviceTypeSlug: string,
|
||||
osDeviceTypeSlug: string,
|
||||
) => {
|
||||
if (appDeviceTypeSlug === osDeviceTypeSlug) {
|
||||
return true;
|
||||
}
|
||||
const sdk = getBalenaSdk();
|
||||
const pineOptions = {
|
||||
$select: 'is_of__cpu_architecture',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
|
||||
const [appDeviceType, osDeviceType] = await Promise.all(
|
||||
[appDeviceTypeSlug, osDeviceTypeSlug].map(
|
||||
(dtSlug) =>
|
||||
sdk.models.deviceType.get(dtSlug, pineOptions) as Promise<
|
||||
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
|
||||
>,
|
||||
),
|
||||
);
|
||||
return sdk.models.os.isArchitectureCompatibleWith(
|
||||
osDeviceType.is_of__cpu_architecture[0].slug,
|
||||
appDeviceType.is_of__cpu_architecture[0].slug,
|
||||
);
|
||||
};
|
||||
export const areDeviceTypesCompatible = (
|
||||
appDeviceType: BalenaSdk.DeviceTypeJson.DeviceType,
|
||||
osDeviceType: BalenaSdk.DeviceTypeJson.DeviceType,
|
||||
) =>
|
||||
getBalenaSdk().models.os.isArchitectureCompatibleWith(
|
||||
osDeviceType.arch,
|
||||
appDeviceType.arch,
|
||||
) && !!appDeviceType.isDependent === !!osDeviceType.isDependent;
|
||||
|
||||
export async function osProgressHandler(step: InitializeEmitter) {
|
||||
step.on('stdout', process.stdout.write.bind(process.stdout));
|
||||
@ -184,7 +155,7 @@ export async function getAppWithArch(
|
||||
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: ['name', 'slug', 'supports_multicontainer'],
|
||||
$select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'],
|
||||
},
|
||||
is_for__device_type: {
|
||||
$select: 'slug',
|
||||
@ -439,11 +410,11 @@ export function getProxyConfig(): ProxyConfig | undefined {
|
||||
|
||||
export const expandForAppName = {
|
||||
$expand: {
|
||||
belongs_to__application: { $select: ['app_name', 'slug'] },
|
||||
belongs_to__application: { $select: ['app_name', 'slug'] as any },
|
||||
is_of__device_type: { $select: 'slug' },
|
||||
is_running__release: { $select: 'commit' },
|
||||
},
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.Device>;
|
||||
} as const;
|
||||
|
||||
export const expandForAppNameAndCpuArch = {
|
||||
$expand: {
|
||||
@ -457,7 +428,7 @@ export const expandForAppNameAndCpuArch = {
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.Device>;
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Use the `readline` library on Windows to install SIGINT handlers.
|
||||
|
@ -137,7 +137,7 @@ adding exception patterns to the applicable .dockerignore file(s), for example
|
||||
- https://www.npmjs.com/package/@balena/dockerignore`;
|
||||
|
||||
export const applicationIdInfo = `\
|
||||
Fleets may be specified by fleet name or slug. Fleet slugs are
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
listed with the \`balena fleets\` command. Note that slugs may change if the
|
||||
fleet is renamed. Fleet names are not unique and may result in "Fleet is
|
||||
@ -145,7 +145,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
|
||||
example if the name clashes with a newly created public fleet, or with fleets
|
||||
from other balena accounts that you may be invited to join under any role.
|
||||
For this reason, fleet names are especially discouraged in scripts (e.g. CI
|
||||
environments).`;
|
||||
environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.`;
|
||||
|
||||
export const applicationNameNote = `\
|
||||
Fleets may be specified by fleet name or slug. Slugs are recommended because
|
||||
@ -169,10 +171,6 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter
|
||||
being a supervisor feature that allows the "balena push" command to push a user's
|
||||
application directly to a device in the local network.`;
|
||||
|
||||
export const secureBootInfo = `\
|
||||
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
|
||||
secure boot and disk encryption.`;
|
||||
|
||||
export const jsonInfo = `\
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because field names are less likely to change in JSON format and because it
|
||||
|
@ -280,64 +280,71 @@ export function inferOrSelectDevice(preferredUuid: string) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Given fleetOrDevice, which may be
|
||||
* - a fleet name
|
||||
* - a fleet slug
|
||||
* Given applicationOrDevice, which may be
|
||||
* - an application name
|
||||
* - an application slug
|
||||
* - an application id (integer)
|
||||
* - a device uuid
|
||||
* Either:
|
||||
* - in case of device uuid, return uuid of device after verifying that it exists and is online.
|
||||
* - in case of fleet, return uuid of device user selects from list of online devices.
|
||||
* - in case of application, return uuid of device user selects from list of online devices.
|
||||
*
|
||||
* TODO: Modify this when app IDs dropped.
|
||||
*/
|
||||
export async function getOnlineTargetDeviceUuid(
|
||||
sdk: BalenaSDK,
|
||||
fleetOrDevice: string,
|
||||
applicationOrDevice: string,
|
||||
) {
|
||||
const logger = (await import('../utils/logger')).getLogger();
|
||||
|
||||
// If looks like UUID, probably device
|
||||
if (validation.validateUuid(fleetOrDevice)) {
|
||||
if (validation.validateUuid(applicationOrDevice)) {
|
||||
let device: Device;
|
||||
try {
|
||||
logger.logDebug(
|
||||
`Trying to fetch device by UUID ${fleetOrDevice} (${typeof fleetOrDevice})`,
|
||||
`Trying to fetch device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
|
||||
);
|
||||
device = await sdk.models.device.get(fleetOrDevice, {
|
||||
device = await sdk.models.device.get(applicationOrDevice, {
|
||||
$select: ['uuid', 'is_online'],
|
||||
});
|
||||
|
||||
if (!device.is_online) {
|
||||
throw new ExpectedError(`Device with UUID ${fleetOrDevice} is offline`);
|
||||
throw new ExpectedError(
|
||||
`Device with UUID ${applicationOrDevice} is offline`,
|
||||
);
|
||||
}
|
||||
|
||||
return device.uuid;
|
||||
} catch (err) {
|
||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||
if (instanceOf(err, BalenaDeviceNotFound)) {
|
||||
logger.logDebug(`Device with UUID ${fleetOrDevice} not found`);
|
||||
// Now try application
|
||||
logger.logDebug(`Device with UUID ${applicationOrDevice} not found`);
|
||||
// Now try app
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not a device UUID, try application
|
||||
let application: Application;
|
||||
// Not a device UUID, try app
|
||||
let app: Application;
|
||||
try {
|
||||
logger.logDebug(`Fetching fleet ${fleetOrDevice}`);
|
||||
logger.logDebug(`Fetching fleet ${applicationOrDevice}`);
|
||||
const { getApplication } = await import('./sdk');
|
||||
application = await getApplication(sdk, fleetOrDevice);
|
||||
app = await getApplication(sdk, applicationOrDevice);
|
||||
} catch (err) {
|
||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||
if (instanceOf(err, BalenaApplicationNotFound)) {
|
||||
throw new ExpectedError(`Fleet or Device not found: ${fleetOrDevice}`);
|
||||
throw new ExpectedError(
|
||||
`Fleet or Device not found: ${applicationOrDevice}`,
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// App found, load its devices
|
||||
const devices = await sdk.models.device.getAllByApplication(application.id, {
|
||||
const devices = await sdk.models.device.getAllByApplication(app.id, {
|
||||
$select: ['device_name', 'uuid'],
|
||||
$filter: { is_online: true },
|
||||
});
|
||||
@ -345,13 +352,13 @@ export async function getOnlineTargetDeviceUuid(
|
||||
// Throw if no devices online
|
||||
if (_.isEmpty(devices)) {
|
||||
throw new ExpectedError(
|
||||
`Fleet ${application.slug} found, but has no devices online.`,
|
||||
`Fleet ${app.slug} found, but has no devices online.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Ask user to select from online devices for fleet
|
||||
// Ask user to select from online devices for application
|
||||
return getCliForm().ask({
|
||||
message: `Select a device on fleet ${application.slug}`,
|
||||
message: `Select a device on fleet ${app.slug}`,
|
||||
type: 'list',
|
||||
default: devices[0].uuid,
|
||||
choices: _.map(devices, (device) => ({
|
||||
|
@ -20,7 +20,7 @@ import { ExpectedError, printErrorMessage } from '../errors';
|
||||
import { getVisuals, stripIndent, getCliForm } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
import { confirm } from './patterns';
|
||||
import { getLocalDeviceCmdStdout, getDeviceOsRelease } from './ssh';
|
||||
import { exec, execBuffered, getDeviceOsRelease } from './ssh';
|
||||
|
||||
const MIN_BALENAOS_VERSION = 'v2.14.0';
|
||||
|
||||
@ -80,12 +80,7 @@ export async function leave(
|
||||
logger.logDebug('Deconfiguring...');
|
||||
await deconfigure(deviceHostnameOrIp);
|
||||
|
||||
logger.logSuccess(stripIndent`
|
||||
Device successfully left the platform. The device will still be listed as part
|
||||
of the fleet, but changes to the fleet will no longer affect the device and its
|
||||
status will eventually be reported as 'Offline'. To irrecoverably delete the
|
||||
device from the fleet, use the 'balena device rm' command or delete it through
|
||||
the balenaCloud web dashboard.`);
|
||||
logger.logSuccess('Device successfully left the platform.');
|
||||
}
|
||||
|
||||
async function execCommand(
|
||||
@ -93,25 +88,20 @@ async function execCommand(
|
||||
cmd: string,
|
||||
msg: string,
|
||||
): Promise<void> {
|
||||
const { Writable } = await import('stream');
|
||||
const through = await import('through2');
|
||||
const visuals = getVisuals();
|
||||
|
||||
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
|
||||
const innerSpinner = spinner.spinner;
|
||||
|
||||
const stream = new Writable({
|
||||
write(_chunk: Buffer, _enc, callback) {
|
||||
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
|
||||
callback();
|
||||
},
|
||||
const stream = through(function (data, _enc, cb) {
|
||||
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
|
||||
cb(null, data);
|
||||
});
|
||||
|
||||
spinner.start();
|
||||
try {
|
||||
await getLocalDeviceCmdStdout(deviceIp, cmd, stream);
|
||||
} finally {
|
||||
spinner.stop();
|
||||
}
|
||||
await exec(deviceIp, cmd, stream);
|
||||
spinner.stop();
|
||||
}
|
||||
|
||||
async function configure(deviceIp: string, config: any): Promise<void> {
|
||||
@ -131,7 +121,7 @@ async function deconfigure(deviceIp: string): Promise<void> {
|
||||
async function assertDeviceIsCompatible(deviceIp: string): Promise<void> {
|
||||
const cmd = 'os-config --version';
|
||||
try {
|
||||
await getLocalDeviceCmdStdout(deviceIp, cmd);
|
||||
await execBuffered(deviceIp, cmd);
|
||||
} catch (err) {
|
||||
if (err instanceof ExpectedError) {
|
||||
throw err;
|
||||
@ -163,61 +153,12 @@ async function getOsVersion(deviceIp: string): Promise<string> {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
const dockerPort = 2375;
|
||||
const dockerTimeout = 2000;
|
||||
|
||||
async function selectLocalBalenaOsDevice(timeout = 4000): Promise<string> {
|
||||
const { discoverLocalBalenaOsDevices } = await import('../utils/discover');
|
||||
const { SpinnerPromise } = getVisuals();
|
||||
const devices = await new SpinnerPromise({
|
||||
promise: discoverLocalBalenaOsDevices(timeout),
|
||||
startMessage: 'Discovering local balenaOS devices..',
|
||||
stopMessage: 'Reporting discovered devices',
|
||||
});
|
||||
|
||||
const responsiveDevices: typeof devices = [];
|
||||
const Docker = await import('docker-toolbelt');
|
||||
await Promise.all(
|
||||
devices.map(async function (device) {
|
||||
const address = device?.address;
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const docker = new Docker({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
});
|
||||
await docker.ping();
|
||||
responsiveDevices.push(device);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (!responsiveDevices.length) {
|
||||
throw new Error('Could not find any local balenaOS devices');
|
||||
}
|
||||
|
||||
return getCliForm().ask({
|
||||
message: 'select a device',
|
||||
type: 'list',
|
||||
default: devices[0].address,
|
||||
choices: responsiveDevices.map((device) => ({
|
||||
name: `${device.host || 'untitled'} (${device.address})`,
|
||||
value: device.address,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async function selectLocalDevice(): Promise<string> {
|
||||
const { forms } = await import('balena-sync');
|
||||
let hostnameOrIp;
|
||||
try {
|
||||
const hostnameOrIp = await selectLocalBalenaOsDevice();
|
||||
hostnameOrIp = await forms.selectLocalBalenaOsDevice();
|
||||
console.error(`==> Selected device: ${hostnameOrIp}`);
|
||||
return hostnameOrIp;
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('could not find any')) {
|
||||
throw new ExpectedError(e);
|
||||
@ -225,6 +166,8 @@ async function selectLocalDevice(): Promise<string> {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return hostnameOrIp;
|
||||
}
|
||||
|
||||
async function selectAppFromList(
|
||||
@ -245,37 +188,31 @@ async function selectAppFromList(
|
||||
|
||||
async function getOrSelectApplication(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
deviceTypeSlug: string,
|
||||
deviceType: string,
|
||||
appName?: string,
|
||||
): Promise<ApplicationWithDeviceType> {
|
||||
const pineOptions = {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
|
||||
const [deviceType, allDeviceTypes] = await Promise.all([
|
||||
sdk.models.deviceType.get(deviceTypeSlug, pineOptions) as Promise<
|
||||
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
|
||||
>,
|
||||
sdk.models.deviceType.getAllSupported(pineOptions) as Promise<
|
||||
Array<BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>>
|
||||
>,
|
||||
]);
|
||||
const _ = await import('lodash');
|
||||
|
||||
const compatibleDeviceTypes = allDeviceTypes
|
||||
.filter((dt) =>
|
||||
sdk.models.os.isArchitectureCompatibleWith(
|
||||
deviceType.is_of__cpu_architecture[0].slug,
|
||||
dt.is_of__cpu_architecture[0].slug,
|
||||
),
|
||||
const allDeviceTypes = await sdk.models.config.getDeviceTypes();
|
||||
const deviceTypeManifest = _.find(allDeviceTypes, { slug: deviceType });
|
||||
if (!deviceTypeManifest) {
|
||||
throw new ExpectedError(`"${deviceType}" is not a valid device type`);
|
||||
}
|
||||
const compatibleDeviceTypes = _(allDeviceTypes)
|
||||
.filter(
|
||||
(dt) =>
|
||||
sdk.models.os.isArchitectureCompatibleWith(
|
||||
deviceTypeManifest.arch,
|
||||
dt.arch,
|
||||
) &&
|
||||
!!dt.isDependent === !!deviceTypeManifest.isDependent &&
|
||||
dt.state !== 'DISCONTINUED',
|
||||
)
|
||||
.map((type) => type.slug);
|
||||
.map((type) => type.slug)
|
||||
.value();
|
||||
|
||||
if (!appName) {
|
||||
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceTypeSlug);
|
||||
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceType);
|
||||
}
|
||||
|
||||
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
|
||||
@ -310,13 +247,13 @@ async function getOrSelectApplication(
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
return await createApplication(sdk, deviceTypeSlug, name);
|
||||
return await createApplication(sdk, deviceType, name);
|
||||
}
|
||||
|
||||
// We've found at least one fleet with the given name.
|
||||
// Filter out fleets for non-matching device types and see what we're left with.
|
||||
const validApplications = applications.filter((app) =>
|
||||
compatibleDeviceTypes.includes(app.is_for__device_type[0].slug),
|
||||
_.includes(compatibleDeviceTypes, app.is_for__device_type[0].slug),
|
||||
);
|
||||
|
||||
if (validApplications.length === 0) {
|
||||
@ -387,7 +324,13 @@ async function createApplication(
|
||||
try {
|
||||
await sdk.models.application.getDirectlyAccessible(appName, {
|
||||
$filter: {
|
||||
slug: { $startswith: `${username!.toLowerCase()}/` },
|
||||
$or: [
|
||||
{ slug: { $startswith: `${username!.toLowerCase()}/` } },
|
||||
// TODO: do we still need the following filter? Is it for
|
||||
// old openBalena instances where slugs were equal to the
|
||||
// app name and did not contain the slash character?
|
||||
{ $not: { slug: { $contains: '/' } } },
|
||||
],
|
||||
},
|
||||
});
|
||||
// TODO: This is the only example in the codebase where `printErrorMessage()`
|
||||
|
@ -21,7 +21,7 @@ import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, stripIndent } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
|
||||
export const QEMU_VERSION = 'v7.0.0+balena1';
|
||||
export const QEMU_VERSION = 'v6.0.0+balena1';
|
||||
export const QEMU_BIN_NAME = 'qemu-execve';
|
||||
|
||||
export function qemuPathInContext(context: string) {
|
||||
|
@ -17,7 +17,7 @@ import type { BalenaSDK } from 'balena-sdk';
|
||||
import * as JSONStream from 'JSONStream';
|
||||
import * as readline from 'readline';
|
||||
import * as request from 'request';
|
||||
import { RegistrySecrets } from '@balena/compose/dist/multibuild';
|
||||
import { RegistrySecrets } from 'resin-multibuild';
|
||||
import type * as Stream from 'stream';
|
||||
import streamToPromise = require('stream-to-promise');
|
||||
import type { Pack } from 'tar-stream';
|
||||
|
@ -24,32 +24,54 @@ import type {
|
||||
|
||||
/**
|
||||
* Get a fleet object, disambiguating the fleet identifier which may be a
|
||||
* a fleet slug or name.
|
||||
* a fleet slug, name or numeric database ID (as a string).
|
||||
* TODO: add support for fleet UUIDs.
|
||||
*/
|
||||
export async function getApplication(
|
||||
sdk: BalenaSDK,
|
||||
nameOrSlug: string,
|
||||
nameOrSlugOrId: string | number,
|
||||
options?: PineOptions<Application>,
|
||||
): Promise<Application> {
|
||||
const { looksLikeFleetSlug } = await import('./validation');
|
||||
if (!looksLikeFleetSlug(nameOrSlug)) {
|
||||
// Not a slug: must be an app name.
|
||||
// TODO: revisit this logic when we add support for fleet UUIDs.
|
||||
return await sdk.models.application.getAppByName(
|
||||
nameOrSlug,
|
||||
const { looksLikeFleetSlug, looksLikeInteger } = await import('./validation');
|
||||
if (
|
||||
typeof nameOrSlugOrId === 'string' &&
|
||||
looksLikeFleetSlug(nameOrSlugOrId)
|
||||
) {
|
||||
return await sdk.models.application.getDirectlyAccessible(
|
||||
nameOrSlugOrId,
|
||||
options,
|
||||
'directly_accessible',
|
||||
);
|
||||
}
|
||||
return await sdk.models.application.getDirectlyAccessible(
|
||||
nameOrSlug,
|
||||
if (typeof nameOrSlugOrId === 'number' || looksLikeInteger(nameOrSlugOrId)) {
|
||||
try {
|
||||
// Test for existence of app with this numerical ID
|
||||
return await sdk.models.application.getDirectlyAccessible(
|
||||
Number(nameOrSlugOrId),
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
if (typeof nameOrSlugOrId === 'number') {
|
||||
throw e;
|
||||
}
|
||||
const { instanceOf } = await import('../errors');
|
||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||
if (!instanceOf(e, BalenaApplicationNotFound)) {
|
||||
throw e;
|
||||
}
|
||||
// App with this numerical ID not found, but there may be an app with this numerical name.
|
||||
}
|
||||
}
|
||||
// Not a slug and not a numeric database ID: must be an app name.
|
||||
// TODO: revisit this logic when we add support for fleet UUIDs.
|
||||
return await sdk.models.application.getAppByName(
|
||||
nameOrSlugOrId,
|
||||
options,
|
||||
'directly_accessible',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a fleet name or slug, return its slug.
|
||||
* Given a fleet name, slug or numeric database ID, return its slug.
|
||||
* This function conditionally makes an async SDK/API call to retrieve the
|
||||
* application object, which can be wasteful if the application object is
|
||||
* required before or after the call to this function. If this is the case,
|
||||
@ -57,15 +79,16 @@ export async function getApplication(
|
||||
*/
|
||||
export async function getFleetSlug(
|
||||
sdk: BalenaSDK,
|
||||
nameOrSlug: string,
|
||||
nameOrSlugOrId: string | number,
|
||||
): Promise<string> {
|
||||
const { looksLikeFleetSlug } = await import('./validation');
|
||||
if (!looksLikeFleetSlug(nameOrSlug)) {
|
||||
// Not a slug: must be an app name.
|
||||
// TODO: revisit this logic when we add support for fleet UUIDs.
|
||||
return (await getApplication(sdk, nameOrSlug)).slug;
|
||||
if (
|
||||
typeof nameOrSlugOrId === 'string' &&
|
||||
looksLikeFleetSlug(nameOrSlugOrId)
|
||||
) {
|
||||
return nameOrSlugOrId.toLowerCase();
|
||||
}
|
||||
return nameOrSlug.toLowerCase();
|
||||
return (await getApplication(sdk, nameOrSlugOrId)).slug;
|
||||
}
|
||||
|
||||
/**
|
||||
|
377
lib/utils/ssh.ts
377
lib/utils/ssh.ts
@ -16,314 +16,147 @@
|
||||
*/
|
||||
import { spawn, StdioOptions } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import { TypedError } from 'typed-error';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
export class SshPermissionDeniedError extends ExpectedError {}
|
||||
export class ExecError extends TypedError {
|
||||
public cmd: string;
|
||||
public exitCode: number;
|
||||
|
||||
export class RemoteCommandError extends ExpectedError {
|
||||
cmd: string;
|
||||
exitCode?: number;
|
||||
exitSignal?: NodeJS.Signals;
|
||||
|
||||
constructor(cmd: string, exitCode?: number, exitSignal?: NodeJS.Signals) {
|
||||
super(sshErrorMessage(cmd, exitSignal, exitCode));
|
||||
constructor(cmd: string, exitCode: number) {
|
||||
super(`Command '${cmd}' failed with error: ${exitCode}`);
|
||||
this.cmd = cmd;
|
||||
this.exitCode = exitCode;
|
||||
this.exitSignal = exitSignal;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SshRemoteCommandOpts {
|
||||
cmd?: string;
|
||||
hostname: string;
|
||||
ignoreStdin?: boolean;
|
||||
port?: number | 'cloud' | 'local';
|
||||
proxyCommand?: string[];
|
||||
username?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export const stdioIgnore: {
|
||||
stdin: 'ignore';
|
||||
stdout: 'ignore';
|
||||
stderr: 'ignore';
|
||||
} = {
|
||||
stdin: 'ignore',
|
||||
stdout: 'ignore',
|
||||
stderr: 'ignore',
|
||||
};
|
||||
|
||||
export function sshArgsForRemoteCommand({
|
||||
cmd = '',
|
||||
hostname,
|
||||
ignoreStdin = false,
|
||||
port,
|
||||
proxyCommand,
|
||||
username = 'root',
|
||||
verbose = false,
|
||||
}: SshRemoteCommandOpts): string[] {
|
||||
port = port === 'local' ? 22222 : port === 'cloud' ? 22 : port;
|
||||
return [
|
||||
...(verbose ? ['-vvv'] : []),
|
||||
...(ignoreStdin ? ['-n'] : []),
|
||||
'-t',
|
||||
...(port ? ['-p', port.toString()] : []),
|
||||
...['-o', 'LogLevel=ERROR'],
|
||||
...['-o', 'StrictHostKeyChecking=no'],
|
||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||
...(proxyCommand && proxyCommand.length
|
||||
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
|
||||
: []),
|
||||
`${username}@${hostname}`,
|
||||
...(cmd ? [cmd] : []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given command on a local balenaOS device over ssh.
|
||||
* @param cmd Shell command to execute on the device
|
||||
* @param hostname Device's hostname or IP address
|
||||
* @param port SSH server TCP port number or 'local' (22222) or 'cloud' (22)
|
||||
* @param stdin Readable stream to pipe to the remote command stdin,
|
||||
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
|
||||
* @param stdout Writeable stream to pipe from the remote command stdout,
|
||||
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
|
||||
* @param stderr Writeable stream to pipe from the remote command stdout,
|
||||
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
|
||||
* @param username SSH username for authorization. With balenaOS 2.44.0 or
|
||||
* later, it can be a balenaCloud username.
|
||||
* @param verbose Produce debugging output
|
||||
*/
|
||||
export async function runRemoteCommand({
|
||||
cmd = '',
|
||||
hostname,
|
||||
port,
|
||||
proxyCommand,
|
||||
stdin = 'inherit',
|
||||
stdout = 'inherit',
|
||||
stderr = 'inherit',
|
||||
username = 'root',
|
||||
verbose = false,
|
||||
}: SshRemoteCommandOpts & {
|
||||
stdin?: 'ignore' | 'inherit' | NodeJS.ReadableStream;
|
||||
stdout?: 'ignore' | 'inherit' | NodeJS.WritableStream;
|
||||
stderr?: 'ignore' | 'inherit' | NodeJS.WritableStream;
|
||||
}): Promise<void> {
|
||||
let ignoreStdin: boolean;
|
||||
if (stdin === 'ignore') {
|
||||
// Set ignoreStdin=true in order for the "ssh -n" option to be used to
|
||||
// prevent the ssh client from using the CLI process stdin. In addition,
|
||||
// stdin must be forced to 'inherit' (if it is not a readable stream) in
|
||||
// order to work around a bug in older versions of the built-in Windows
|
||||
// 10 ssh client that otherwise prints the following to stderr and
|
||||
// hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
|
||||
// They actually fixed the bug in newer versions of the ssh client:
|
||||
// https://github.com/PowerShell/Win32-OpenSSH/issues/856 but users
|
||||
// have to manually download and install a new client.
|
||||
ignoreStdin = true;
|
||||
stdin = 'inherit';
|
||||
} else {
|
||||
ignoreStdin = false;
|
||||
}
|
||||
export async function exec(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
stdout?: NodeJS.WritableStream,
|
||||
): Promise<void> {
|
||||
const { which } = await import('./which');
|
||||
const program = await which('ssh');
|
||||
const args = sshArgsForRemoteCommand({
|
||||
const args = [
|
||||
'-n',
|
||||
'-t',
|
||||
'-p',
|
||||
'22222',
|
||||
'-o',
|
||||
'LogLevel=ERROR',
|
||||
'-o',
|
||||
'StrictHostKeyChecking=no',
|
||||
'-o',
|
||||
'UserKnownHostsFile=/dev/null',
|
||||
`root@${deviceIp}`,
|
||||
cmd,
|
||||
hostname,
|
||||
ignoreStdin,
|
||||
port,
|
||||
proxyCommand,
|
||||
username,
|
||||
verbose,
|
||||
});
|
||||
|
||||
];
|
||||
if (process.env.DEBUG) {
|
||||
const logger = (await import('./logger')).getLogger();
|
||||
logger.logDebug(`Executing [${program},${args}]`);
|
||||
}
|
||||
|
||||
// Note: stdin must be 'inherit' to workaround a bug in older versions of
|
||||
// the built-in Windows 10 ssh client that otherwise prints the following
|
||||
// to stderr and hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
|
||||
// They fixed the bug in newer versions of the ssh client:
|
||||
// https://github.com/PowerShell/Win32-OpenSSH/issues/856
|
||||
// but users whould have to manually download and install a new client.
|
||||
// Note that "ssh -n" does not solve the problem, but should in theory
|
||||
// prevent the ssh client from using the CLI process stdin, even if it
|
||||
// is connected with 'inherit'.
|
||||
const stdio: StdioOptions = [
|
||||
typeof stdin === 'string' ? stdin : 'pipe',
|
||||
typeof stdout === 'string' ? stdout : 'pipe',
|
||||
typeof stderr === 'string' ? stderr : 'pipe',
|
||||
'inherit',
|
||||
stdout ? 'pipe' : 'inherit',
|
||||
'inherit',
|
||||
];
|
||||
let exitCode: number | undefined;
|
||||
let exitSignal: NodeJS.Signals | undefined;
|
||||
try {
|
||||
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
|
||||
const ps = spawn(program, args, { stdio })
|
||||
.on('error', reject)
|
||||
.on('close', (code, signal) =>
|
||||
resolve([code ?? undefined, signal ?? undefined]),
|
||||
);
|
||||
|
||||
if (ps.stdin && stdin && typeof stdin !== 'string') {
|
||||
stdin.pipe(ps.stdin);
|
||||
}
|
||||
if (ps.stdout && stdout && typeof stdout !== 'string') {
|
||||
ps.stdout.pipe(stdout);
|
||||
}
|
||||
if (ps.stderr && stderr && typeof stderr !== 'string') {
|
||||
ps.stderr.pipe(stderr);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = [
|
||||
`ssh failed with exit code=${exitCode} signal=${exitSignal}:`,
|
||||
`[${program}, ${args.join(', ')}]`,
|
||||
...(error ? [`${error}`] : []),
|
||||
];
|
||||
throw new ExpectedError(msg.join('\n'));
|
||||
}
|
||||
if (exitCode || exitSignal) {
|
||||
throw new RemoteCommandError(cmd, exitCode, exitSignal);
|
||||
const exitCode = await new Promise<number>((resolve, reject) => {
|
||||
const ps = spawn(program, args, { stdio })
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
|
||||
if (stdout && ps.stdout) {
|
||||
ps.stdout.pipe(stdout);
|
||||
}
|
||||
});
|
||||
if (exitCode !== 0) {
|
||||
throw new ExecError(cmd, exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given command on a local balenaOS device over ssh.
|
||||
* Capture stdout and/or stderr to Buffers and return them.
|
||||
*
|
||||
* @param deviceIp IP address of the local device
|
||||
* @param cmd Shell command to execute on the device
|
||||
* @param opts Options
|
||||
* @param opts.username SSH username for authorization. With balenaOS 2.44.0 or
|
||||
* later, it may be a balenaCloud username. Otherwise, 'root'.
|
||||
* @param opts.stdin Passed through to the runRemoteCommand function
|
||||
* @param opts.stdout If 'capture', capture stdout to a Buffer.
|
||||
* @param opts.stderr If 'capture', capture stdout to a Buffer.
|
||||
*/
|
||||
export async function getRemoteCommandOutput({
|
||||
cmd,
|
||||
hostname,
|
||||
port,
|
||||
proxyCommand,
|
||||
stdin = 'ignore',
|
||||
stdout = 'capture',
|
||||
stderr = 'capture',
|
||||
username = 'root',
|
||||
verbose = false,
|
||||
}: SshRemoteCommandOpts & {
|
||||
stdin?: 'ignore' | 'inherit' | NodeJS.ReadableStream;
|
||||
stdout?: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream;
|
||||
stderr?: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream;
|
||||
}): Promise<{ stdout: Buffer; stderr: Buffer }> {
|
||||
const { Writable } = await import('stream');
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
const stderrChunks: Buffer[] = [];
|
||||
const stdoutStream = new Writable({
|
||||
write(chunk: Buffer, _enc, callback) {
|
||||
stdoutChunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const stderrStream = new Writable({
|
||||
write(chunk: Buffer, _enc, callback) {
|
||||
stderrChunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
await runRemoteCommand({
|
||||
cmd,
|
||||
hostname,
|
||||
port,
|
||||
proxyCommand,
|
||||
stdin,
|
||||
stdout: stdout === 'capture' ? stdoutStream : stdout,
|
||||
stderr: stderr === 'capture' ? stderrStream : stderr,
|
||||
username,
|
||||
verbose,
|
||||
});
|
||||
return {
|
||||
stdout: Buffer.concat(stdoutChunks),
|
||||
stderr: Buffer.concat(stderrChunks),
|
||||
};
|
||||
}
|
||||
|
||||
/** Convenience wrapper for getRemoteCommandOutput */
|
||||
export async function getLocalDeviceCmdStdout(
|
||||
hostname: string,
|
||||
export async function execBuffered(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
stdout: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream = 'capture',
|
||||
): Promise<Buffer> {
|
||||
const port = 'local';
|
||||
return (
|
||||
await getRemoteCommandOutput({
|
||||
cmd,
|
||||
hostname,
|
||||
port,
|
||||
stdout,
|
||||
stderr: 'inherit',
|
||||
username: await findBestUsernameForDevice(hostname, port),
|
||||
})
|
||||
).stdout;
|
||||
enc?: string,
|
||||
): Promise<string> {
|
||||
const through = await import('through2');
|
||||
const buffer: string[] = [];
|
||||
await exec(
|
||||
deviceIp,
|
||||
cmd,
|
||||
through(function (data, _enc, cb) {
|
||||
buffer.push(data.toString(enc));
|
||||
cb();
|
||||
}),
|
||||
);
|
||||
return buffer.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a trivial 'exit 0' command over ssh on the target hostname (typically the
|
||||
* IP address of a local device) with the 'root' username, in order to determine
|
||||
* whether root authentication suceeds. It should succeed with development
|
||||
* variants of balenaOS and fail with production variants, unless a ssh key was
|
||||
* added to the device's 'config.json' file.
|
||||
* @return True if succesful, false on any errors.
|
||||
*/
|
||||
export const isRootUserGood = _.memoize(async (hostname: string, port) => {
|
||||
try {
|
||||
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...stdioIgnore });
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Determine whether the given local device (hostname or IP address) should be
|
||||
* accessed as the 'root' user or as a regular cloud user (balenaCloud or
|
||||
* openBalena). Where possible, the root user is preferable because:
|
||||
* - It allows ssh to be used in air-gapped scenarios (no internet access).
|
||||
* Logging in as a regular user requires the device to fetch public keys from
|
||||
* the cloud backend.
|
||||
* - Root authentication is significantly faster for local devices (a fraction
|
||||
* of a second versus 5+ seconds).
|
||||
* - Non-root authentication requires balenaOS v2.44.0 or later, so not (yet)
|
||||
* universally possible.
|
||||
*/
|
||||
export const findBestUsernameForDevice = _.memoize(
|
||||
async (hostname: string, port): Promise<string> => {
|
||||
let username: string | undefined;
|
||||
if (await isRootUserGood(hostname, port)) {
|
||||
username = 'root';
|
||||
} else {
|
||||
const { getCachedUsername } = await import('./bootstrap');
|
||||
username = (await getCachedUsername())?.username;
|
||||
}
|
||||
if (!username) {
|
||||
const { stripIndent } = await import('./lazy');
|
||||
throw new ExpectedError(stripIndent`
|
||||
SSH authentication failed for 'root@${hostname}'.
|
||||
Please login with 'balena login' for alternative authentication.`);
|
||||
}
|
||||
return username;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Return a device's balenaOS release by executing 'cat /etc/os-release'
|
||||
* over ssh to the given deviceIp address. The result is cached with
|
||||
* lodash's memoize.
|
||||
*/
|
||||
export const getDeviceOsRelease = _.memoize(async (hostname: string) =>
|
||||
(await getLocalDeviceCmdStdout(hostname, 'cat /etc/os-release')).toString(),
|
||||
export const getDeviceOsRelease = _.memoize(async (deviceIp: string) =>
|
||||
execBuffered(deviceIp, 'cat /etc/os-release'),
|
||||
);
|
||||
|
||||
function sshErrorMessage(cmd: string, exitSignal?: string, exitCode?: number) {
|
||||
// TODO: consolidate the various forms of executing ssh child processes
|
||||
// in the CLI, like exec and spawn, starting with the files:
|
||||
// lib/actions/ssh.ts
|
||||
// lib/utils/ssh.ts
|
||||
// lib/utils/device/ssh.ts
|
||||
|
||||
/**
|
||||
* Obtain the full path for ssh using which, then spawn a child process.
|
||||
* - If the child process returns error code 0, return the function normally
|
||||
* (do not throw an error).
|
||||
* - If the child process returns a non-zero error code, set process.exitCode
|
||||
* to that error code, and throw ExpectedError with a warning message.
|
||||
* - If the child process is terminated by a process signal, set
|
||||
* process.exitCode = 1, and throw ExpectedError with a warning message.
|
||||
*/
|
||||
export async function spawnSshAndThrowOnError(
|
||||
args: string[],
|
||||
options?: import('child_process').SpawnOptions,
|
||||
) {
|
||||
const { whichSpawn } = await import('./which');
|
||||
const [exitCode, exitSignal] = await whichSpawn(
|
||||
'ssh',
|
||||
args,
|
||||
options,
|
||||
true, // returnExitCodeOrSignal
|
||||
);
|
||||
if (exitCode || exitSignal) {
|
||||
// ssh returns a wide range of exit codes, including return codes of
|
||||
// interactive shells. For example, if the user types CTRL-C on an
|
||||
// interactive shell and then `exit`, ssh returns error code 130.
|
||||
// Another example, typing "exit 1" on an interactive shell causes ssh
|
||||
// to return exit code 1. In these cases, print a short one-line warning
|
||||
// message, and exits the CLI process with the same error code.
|
||||
process.exitCode = exitCode;
|
||||
throw new ExpectedError(sshErrorMessage(exitSignal, exitCode));
|
||||
}
|
||||
}
|
||||
|
||||
function sshErrorMessage(exitSignal?: string, exitCode?: number) {
|
||||
const msg: string[] = [];
|
||||
cmd = cmd ? `Remote command "${cmd}"` : 'Process';
|
||||
if (exitSignal) {
|
||||
msg.push(`SSH: ${cmd} terminated with signal "${exitSignal}"`);
|
||||
msg.push(`Warning: ssh process was terminated with signal "${exitSignal}"`);
|
||||
} else {
|
||||
msg.push(`SSH: ${cmd} exited with non-zero status code "${exitCode}"`);
|
||||
msg.push(`Warning: ssh process exited with non-zero code "${exitCode}"`);
|
||||
switch (exitCode) {
|
||||
case 255:
|
||||
msg.push(`
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2022 Balena
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -19,10 +19,8 @@ import * as UpdateNotifier from 'update-notifier';
|
||||
|
||||
import packageJSON = require('../../package.json');
|
||||
|
||||
// Check for an update at most once a day. 1 day granularity should be
|
||||
// enough, rather than every run. Note because we show the information
|
||||
// from the *last* time we ran, if the cli has not been run for a while
|
||||
// the update info can be out of date.
|
||||
// Check for an update once a day. 1 day granularity should be
|
||||
// enough, rather than every run.
|
||||
const balenaUpdateInterval = 1000 * 60 * 60 * 24 * 1;
|
||||
|
||||
let notifier: UpdateNotifier.UpdateNotifier;
|
||||
@ -42,35 +40,14 @@ export function notify() {
|
||||
}
|
||||
}
|
||||
const up = notifier.update;
|
||||
const message = up && getNotifierMessage(up);
|
||||
if (message) {
|
||||
notifier.notify({ defer: false, message });
|
||||
if (
|
||||
up &&
|
||||
(require('semver') as typeof import('semver')).lt(up.current, up.latest)
|
||||
) {
|
||||
notifier.notify({
|
||||
defer: false,
|
||||
message: `Update available ${up.current} → ${up.latest}\n
|
||||
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getNotifierMessage(updateInfo: UpdateNotifier.UpdateInfo) {
|
||||
const semver = require('semver') as typeof import('semver');
|
||||
const message: string[] = [];
|
||||
const [current, latest] = [updateInfo.current, updateInfo.latest];
|
||||
|
||||
if (semver.lt(current, latest)) {
|
||||
message.push(
|
||||
`Update available ${current} → ${latest}`,
|
||||
'https://github.com/balena-io/balena-cli/blob/master/INSTALL.md',
|
||||
);
|
||||
const currentMajor = semver.major(current);
|
||||
const latestMajor = semver.major(latest);
|
||||
if (currentMajor !== latestMajor) {
|
||||
message.push(
|
||||
'',
|
||||
`Check the v${latestMajor} release notes at:`,
|
||||
getReleaseNotesUrl(latestMajor),
|
||||
);
|
||||
}
|
||||
}
|
||||
return message.join('\n');
|
||||
}
|
||||
|
||||
function getReleaseNotesUrl(majorVersion: number) {
|
||||
return `https://github.com/balena-io/balena-cli/wiki/CLI-v${majorVersion}-Release-Notes`;
|
||||
}
|
||||
|
@ -21,3 +21,13 @@ import { version } from '../../package.json';
|
||||
export function isVersionGTE(v: string): boolean {
|
||||
return semver.gte(process.env.BALENA_CLI_VERSION_OVERRIDE || version, v);
|
||||
}
|
||||
|
||||
let v14: boolean;
|
||||
|
||||
/** Feature switch for the next major version of the CLI */
|
||||
export function isV14(): boolean {
|
||||
if (v14 === undefined) {
|
||||
v14 = isVersionGTE('14.0.0');
|
||||
}
|
||||
return v14;
|
||||
}
|
||||
|
@ -95,3 +95,52 @@ export async function which(
|
||||
}
|
||||
return programPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call which(programName) and spawn() with the given arguments.
|
||||
*
|
||||
* If returnExitCodeOrSignal is true, the returned promise will resolve to
|
||||
* an array [code, signal] with the child process exit code number or exit
|
||||
* signal string respectively (as provided by the spawn close event).
|
||||
*
|
||||
* If returnExitCodeOrSignal is false, the returned promise will reject with
|
||||
* a custom error if the child process returns a non-zero exit code or a
|
||||
* non-empty signal string (as reported by the spawn close event).
|
||||
*
|
||||
* In either case and if spawn itself emits an error event or fails synchronously,
|
||||
* the returned promise will reject with a custom error that includes the error
|
||||
* message of spawn's error.
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args: string[],
|
||||
options: import('child_process').SpawnOptions = { stdio: 'inherit' },
|
||||
returnExitCodeOrSignal = false,
|
||||
): Promise<[number | undefined, string | undefined]> {
|
||||
const { spawn } = await import('child_process');
|
||||
const program = await which(programName);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] [${program}, ${args.join(', ')}]`);
|
||||
}
|
||||
let error: Error | undefined;
|
||||
let exitCode: number | undefined;
|
||||
let exitSignal: string | undefined;
|
||||
try {
|
||||
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
|
||||
spawn(program, args, options)
|
||||
.on('error', reject)
|
||||
.on('close', (code, signal) => resolve([code, signal]));
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
if (error || (!returnExitCodeOrSignal && (exitCode || exitSignal))) {
|
||||
const msg = [
|
||||
`${programName} failed with exit code=${exitCode} signal=${exitSignal}:`,
|
||||
`[${program}, ${args.join(', ')}]`,
|
||||
...(error ? [`${error}`] : []),
|
||||
];
|
||||
throw new Error(msg.join('\n'));
|
||||
}
|
||||
return [exitCode, exitSignal];
|
||||
}
|
||||
|
29788
npm-shrinkwrap.json
generated
29788
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "16.2.0",
|
||||
"version": "13.1.13",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -27,8 +27,9 @@
|
||||
"scripts": [
|
||||
"build/**/*.js",
|
||||
"node_modules/balena-sdk/es2018/index.js",
|
||||
"node_modules/balena-sync/build/**/*.js",
|
||||
"node_modules/pinejs-client-request/node_modules/pinejs-client-core/es2018/index.js",
|
||||
"node_modules/@balena/compose/dist/parse/schemas/*.json"
|
||||
"node_modules/resin-compose-parse/build/schemas/*.json"
|
||||
],
|
||||
"assets": [
|
||||
"build/auth/pages/*.ejs",
|
||||
@ -89,11 +90,12 @@
|
||||
"author": "Balena Inc. (https://balena.io/)",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16 <18"
|
||||
"node": ">=12.8.0 <13.0.0",
|
||||
"npm": "<7.0.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "node automation/check-npm-version.js && ts-node automation/check-doc.ts"
|
||||
"pre-commit": "node automation/check-npm-version.js && node automation/check-doc.js"
|
||||
}
|
||||
},
|
||||
"oclif": {
|
||||
@ -113,7 +115,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@balena/lint": "^6.2.2",
|
||||
"@balena/lint": "^6.2.0",
|
||||
"@oclif/config": "^1.18.2",
|
||||
"@oclif/parser": "^3.8.6",
|
||||
"@octokit/plugin-throttling": "^3.5.1",
|
||||
@ -125,14 +127,13 @@
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/cli-truncate": "^2.0.0",
|
||||
"@types/common-tags": "^1.8.1",
|
||||
"@types/dockerode": "^3.3.9",
|
||||
"@types/dockerode": "^3.3.0",
|
||||
"@types/ejs": "^3.1.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/global-agent": "^2.1.1",
|
||||
"@types/global-tunnel-ng": "^2.1.1",
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"@types/inquirer": "^7.3.3",
|
||||
"@types/intercept-stdout": "^0.1.0",
|
||||
"@types/is-root": "^2.1.2",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
@ -146,7 +147,7 @@
|
||||
"@types/ndjson": "^2.0.1",
|
||||
"@types/net-keepalive": "^0.4.1",
|
||||
"@types/nock": "^11.1.0",
|
||||
"@types/node": "^16.18.25",
|
||||
"@types/node": "^12.20.42",
|
||||
"@types/node-cleanup": "^2.1.2",
|
||||
"@types/parse-link-header": "^1.0.1",
|
||||
"@types/prettyjson": "^0.0.30",
|
||||
@ -154,7 +155,6 @@
|
||||
"@types/request": "^2.48.7",
|
||||
"@types/rewire": "^2.5.28",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"@types/sinon": "^10.0.6",
|
||||
"@types/split": "^1.0.0",
|
||||
@ -183,17 +183,16 @@
|
||||
"mocha": "^8.4.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"nock": "^13.2.1",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"pkg": "^5.8.1",
|
||||
"parse-link-header": "^1.0.1",
|
||||
"pkg": "^5.5.1",
|
||||
"publish-release": "^1.6.1",
|
||||
"rewire": "^5.0.0",
|
||||
"simple-git": "^3.14.1",
|
||||
"simple-git": "^2.48.0",
|
||||
"sinon": "^11.1.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^5.0.2"
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@balena/compose": "^2.2.1",
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@oclif/command": "^1.8.16",
|
||||
@ -201,32 +200,35 @@
|
||||
"@sentry/node": "^6.16.1",
|
||||
"@types/fast-levenshtein": "0.0.1",
|
||||
"@types/update-notifier": "^4.1.1",
|
||||
"JSONStream": "^1.0.3",
|
||||
"balena-config-json": "^4.2.0",
|
||||
"balena-device-init": "^6.0.0",
|
||||
"balena-errors": "^4.7.3",
|
||||
"balena-errors": "^4.7.1",
|
||||
"balena-image-fs": "^7.0.6",
|
||||
"balena-image-manager": "^8.0.0",
|
||||
"balena-preload": "^13.0.0",
|
||||
"balena-sdk": "^16.44.2",
|
||||
"balena-image-manager": "^7.1.1",
|
||||
"balena-preload": "^12.0.0",
|
||||
"balena-release": "^3.2.0",
|
||||
"balena-sdk": "^16.9.0",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^4.0.7",
|
||||
"balena-settings-storage": "^7.0.0",
|
||||
"balena-sync": "^11.0.2",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.19.1",
|
||||
"chalk": "^3.0.0",
|
||||
"chokidar": "^3.5.2",
|
||||
"cli-truncate": "^2.1.0",
|
||||
"cli-ux": "^5.5.1",
|
||||
"color-hash": "^1.1.1",
|
||||
"cli-ux": "^6.0.5",
|
||||
"columnify": "^1.5.2",
|
||||
"common-tags": "^1.7.2",
|
||||
"denymount": "^2.3.0",
|
||||
"docker-modem": "3.0.0",
|
||||
"docker-progress": "^5.1.3",
|
||||
"docker-toolbelt": "^3.3.10",
|
||||
"docker-progress": "^5.0.1",
|
||||
"docker-qemu-transpose": "^1.1.1",
|
||||
"dockerode": "^3.3.1",
|
||||
"ejs": "^3.1.6",
|
||||
"etcher-sdk": "^8.5.3",
|
||||
"etcher-sdk": "^6.2.1",
|
||||
"event-stream": "3.3.4",
|
||||
"express": "^4.17.2",
|
||||
"fast-boot2": "^1.1.0",
|
||||
@ -242,7 +244,6 @@
|
||||
"is-elevated": "^3.0.0",
|
||||
"is-root": "^2.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"JSONStream": "^1.0.3",
|
||||
"klaw": "^3.0.0",
|
||||
"livepush": "^3.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
@ -261,9 +262,10 @@
|
||||
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
|
||||
"request": "^2.88.2",
|
||||
"resin-cli-form": "^2.0.2",
|
||||
"resin-cli-visuals": "^1.8.2",
|
||||
"resin-discoverable-services": "^2.0.4",
|
||||
"resin-cli-visuals": "^1.8.0",
|
||||
"resin-compose-parse": "^2.1.3",
|
||||
"resin-doodles": "^0.2.0",
|
||||
"resin-multibuild": "^4.12.2",
|
||||
"resin-stream-logger": "^0.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
@ -285,6 +287,6 @@
|
||||
"windosu": "^0.3.0"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2023-05-19T18:11:55.798Z"
|
||||
"publishedAt": "2022-02-10T11:50:34.458Z"
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,24 @@
|
||||
diff --git a/node_modules/@oclif/parser/lib/errors.js b/node_modules/@oclif/parser/lib/errors.js
|
||||
index 39936e3..23e3925 100644
|
||||
index 0c93a81..95d06be 100644
|
||||
--- a/node_modules/@oclif/parser/lib/errors.js
|
||||
+++ b/node_modules/@oclif/parser/lib/errors.js
|
||||
@@ -14,7 +14,8 @@ const m = deps_1.default()
|
||||
@@ -13,7 +13,8 @@ const m = deps_1.default()
|
||||
.add('list', () => require('./list'));
|
||||
class CLIParseError extends errors_1.CLIError {
|
||||
constructor(options) {
|
||||
- options.message += '\nSee more help with --help';
|
||||
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
|
||||
+ options.message += `\nSee more help with ${help}`;
|
||||
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
|
||||
+ options.message += `\nSee more help with ${help}`;
|
||||
super(options.message);
|
||||
this.parse = options.parse;
|
||||
}
|
||||
@@ -35,22 +36,24 @@ class InvalidArgsSpecError extends CLIParseError {
|
||||
@@ -34,22 +35,24 @@ class InvalidArgsSpecError extends CLIParseError {
|
||||
exports.InvalidArgsSpecError = InvalidArgsSpecError;
|
||||
class RequiredArgsError extends CLIParseError {
|
||||
constructor({ args, parse }) {
|
||||
- let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}`;
|
||||
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
|
||||
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
|
||||
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
|
||||
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
|
||||
const namedArgs = args.filter(a => a.name);
|
||||
if (namedArgs.length > 0) {
|
||||
const list = m.list.renderList(namedArgs.map(a => [a.name, a.description]));
|
||||
@ -32,7 +32,7 @@ index 39936e3..23e3925 100644
|
||||
exports.RequiredArgsError = RequiredArgsError;
|
||||
class RequiredFlagError extends CLIParseError {
|
||||
constructor({ flag, parse }) {
|
||||
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
|
||||
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
|
||||
const usage = m.list.renderList(m.help.flagUsages([flag], { displayRequired: false }));
|
||||
const message = `Missing required flag:\n${usage}`;
|
||||
- super({ parse, message });
|
||||
@ -41,15 +41,15 @@ index 39936e3..23e3925 100644
|
||||
}
|
||||
}
|
||||
diff --git a/node_modules/@oclif/parser/lib/list.js b/node_modules/@oclif/parser/lib/list.js
|
||||
index 9d020b7..6ea9eb9 100644
|
||||
index 3907cc0..b689ca1 100644
|
||||
--- a/node_modules/@oclif/parser/lib/list.js
|
||||
+++ b/node_modules/@oclif/parser/lib/list.js
|
||||
@@ -22,7 +22,7 @@ function renderList(items) {
|
||||
@@ -21,7 +21,7 @@ function renderList(items) {
|
||||
}
|
||||
left = left.padEnd(maxLength);
|
||||
right = linewrap(maxLength + 2, right);
|
||||
- return `${left} ${right}`;
|
||||
+ return `${left} : ${right}`;
|
||||
+ return `${left} : ${right}`;
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user